From d40259681fd4711f25d364571a497def207ec473 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 19 May 2026 11:37:48 -0500 Subject: [PATCH 01/12] imrpove language of gallery landing page --- docs/gallery.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/gallery.md b/docs/gallery.md index c14dfa8a4..961d8d394 100644 --- a/docs/gallery.md +++ b/docs/gallery.md @@ -4,9 +4,17 @@ Examples of napari usage. -All examples in this gallery can be downloaded as Python scripts or Jupyter -notebooks to be executed locally. Check out [](launch-jupyter) for more details -on using napari in Jupyter notebooks. +Each example page includes downloads for both the source `.py` file and a +generated Jupyter notebook. If you download the Python script, you can run it +with `napari path/to/my_example.py` or drag the `.py` file onto a running napari viewer +to execute it there. Some examples may require additional dependencies beyond +napari itself, so be sure to check the example's source code for any additional +requirements if you see an error when trying to run it. +Check out [](launch-jupyter) for more details on using +napari in Jupyter notebooks. + +Use the tag index below to browse examples by topic, or jump straight into the +gallery grid. ```{note} From 431c86946be733389e7995c45e8e0c2bd8056f18 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 19 May 2026 11:38:13 -0500 Subject: [PATCH 02/12] WIP initial LLM working strategy --- docs/_static/custom.css | 27 +++++++++++++++++ docs/conf.py | 64 +++++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index e5c50e450..918057251 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -44,6 +44,33 @@ html[data-theme="dark"] div.sphx-glr-download a:hover { background-color: #222832 !important; } +.napari-gallery-downloads { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0 0 1.25rem; +} + +.napari-gallery-downloads .sphx-glr-download { + margin: 0; +} + +html[data-theme="light"] .napari-gallery-downloads div.sphx-glr-download a { + border-color: #d7deea !important; +} + +html[data-theme="dark"] .napari-gallery-downloads div.sphx-glr-download a { + border-color: #4f5f79 !important; + color: #fff; +} + +.sphx-glr-download-link-note, +.sphx-glr-download-jupyter, +.sphx-glr-download-python, +.sphx-glr-download-zip { + display: none; +} + /* Version warning banner color */ #bd-header-version-warning { background-color: color-mix(in srgb, var(--pst-color-secondary-bg), transparent 30%); diff --git a/docs/conf.py b/docs/conf.py index 569b5b0da..362b9bf76 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -396,6 +396,60 @@ def napari_scraper(block, block_vars, gallery_conf): return scrapers.figure_rst(img_paths, gallery_conf['src_dir']) +GALLERY_DOWNLOAD_NOTE_RE = re.compile( + r'\n\.\. only:: html\n\n' + r' \.\. note::\n' + r' :class: sphx-glr-download-link-note\n\n' + r' :ref:`Go to the end ]+>`\n' + r' to download the full example as a Python script or as a\n' + r' Jupyter notebook\.\.?\n', + re.MULTILINE, +) +GALLERY_TITLE_RE = re.compile(r'^(?P.+\n=+\n)', re.MULTILINE) + + +def add_gallery_download_buttons(app, docname, source): + """Add compact top-of-page download links to generated gallery examples.""" + if not docname.startswith('gallery/'): + return + if docname.endswith('/index') or docname.endswith('sg_execution_times'): + return + + content = source[0] + if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: + return + + content = GALLERY_DOWNLOAD_NOTE_RE.sub('\n', content, count=1) + if 'napari-gallery-downloads' in content: + source[0] = content + return + + example_name = Path(docname).name + title_match = GALLERY_TITLE_RE.search(content) + if title_match is None: + source[0] = content + return + + download_block = f""" +.. only:: html + + .. container:: napari-gallery-downloads + + .. container:: sphx-glr-download + + :download:`Python (.py) <{example_name}.py>` + + .. container:: sphx-glr-download + + :download:`Notebook (.ipynb) <{example_name}.ipynb>` +""" + source[0] = ( + content[: title_match.end()] + + download_block + + content[title_match.end() :] + ) + + gen_rst.EXAMPLE_HEADER = """ .. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. @@ -403,15 +457,6 @@ def napari_scraper(block, block_vars, gallery_conf): .. "{0}" .. LINE NUMBERS ARE GIVEN BELOW. -.. only:: html - - .. note:: - :class: sphx-glr-download-link-note - - :ref:`Go to the end <sphx_glr_download_{1}>` - to download the full example as a Python script or as a - Jupyter notebook.{2} - .. rst-class:: sphx-glr-example-title .. _sphx_glr_{1}: @@ -549,6 +594,7 @@ def setup(app): """ app.registry.source_suffix.pop('.ipynb', None) app.connect('source-read', add_google_calendar_secrets) + app.connect('source-read', add_gallery_download_buttons) app.connect('linkcheck-process-uri', rewrite_github_anchor) app.connect('autodoc-process-docstring', qt_docstrings) From fc1477b4f50d9d120329e7389d4c873b2f1ce27f Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 19 May 2026 12:13:29 -0500 Subject: [PATCH 03/12] simplify imlpementation and css --- docs/_static/custom.css | 9 --------- docs/conf.py | 24 +++++++----------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 918057251..0eed907b2 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -55,15 +55,6 @@ html[data-theme="dark"] div.sphx-glr-download a:hover { margin: 0; } -html[data-theme="light"] .napari-gallery-downloads div.sphx-glr-download a { - border-color: #d7deea !important; -} - -html[data-theme="dark"] .napari-gallery-downloads div.sphx-glr-download a { - border-color: #4f5f79 !important; - color: #fff; -} - .sphx-glr-download-link-note, .sphx-glr-download-jupyter, .sphx-glr-download-python, diff --git a/docs/conf.py b/docs/conf.py index 362b9bf76..43b7cdf72 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -396,20 +396,16 @@ def napari_scraper(block, block_vars, gallery_conf): return scrapers.figure_rst(img_paths, gallery_conf['src_dir']) -GALLERY_DOWNLOAD_NOTE_RE = re.compile( - r'\n\.\. only:: html\n\n' - r' \.\. note::\n' - r' :class: sphx-glr-download-link-note\n\n' - r' :ref:`Go to the end <sphx_glr_download_[^>]+>`\n' - r' to download the full example as a Python script or as a\n' - r' Jupyter notebook\.\.?\n', - re.MULTILINE, -) -GALLERY_TITLE_RE = re.compile(r'^(?P<title>.+\n=+\n)', re.MULTILINE) +GALLERY_TITLE_RE = re.compile(r'^.+\n=+\n', re.MULTILINE) def add_gallery_download_buttons(app, docname, source): - """Add compact top-of-page download links to generated gallery examples.""" + """Add compact top-of-page download links to generated gallery examples. + + The title of the example is used to determine where to insert the download links, + which are only added to generated gallery examples. + The links are added after the title. + """ if not docname.startswith('gallery/'): return if docname.endswith('/index') or docname.endswith('sg_execution_times'): @@ -419,15 +415,9 @@ def add_gallery_download_buttons(app, docname, source): if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: return - content = GALLERY_DOWNLOAD_NOTE_RE.sub('\n', content, count=1) - if 'napari-gallery-downloads' in content: - source[0] = content - return - example_name = Path(docname).name title_match = GALLERY_TITLE_RE.search(content) if title_match is None: - source[0] = content return download_block = f""" From 69a368b10abe3a4582b93223fe264a91bb109a94 Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 19 May 2026 12:40:13 -0500 Subject: [PATCH 04/12] merge in deps check branch --- docs/conf.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 43b7cdf72..dc55bd919 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,7 @@ # -- Imports -------------------------------------------------------------- +import ast import logging import json import os @@ -438,6 +439,56 @@ def add_gallery_download_buttons(app, docname, source): + download_block + content[title_match.end() :] ) +# --------------------------------------------------------------------------- +# Gallery extra-dependency detection +# --------------------------------------------------------------------------- + +# Packages from napari's [dependency-groups] gallery that are NOT included in +# a standard `napari[all]` install. Maps import-root -> pip package name. +_GALLERY_EXTRAS: dict[str, str] = { + 'glasbey': 'glasbey', + 'matplotlib': 'matplotlib', + 'meshio': 'meshio', + 'nilearn': 'nilearn', + 'trackastra': 'trackastra', + 'xarray': 'xarray', +} + +# Matches the source .py path embedded in every sphinx-gallery RST header. +_GALLERY_SOURCE_PATH_RE = re.compile(r'\.\. "(.+\.py)"\n') + +# Matches the first top-level RST title (text + === underline). +_GALLERY_TITLE_RE = re.compile(r'^.+\n=+\n', re.MULTILINE) + + +def _get_example_extra_deps(content: str, srcdir: str) -> list[str]: + """Return extra pip packages needed by a gallery example beyond napari[all].""" + path_match = _GALLERY_SOURCE_PATH_RE.search(content) + if path_match is None: + return [] + py_path = Path(path_match.group(1)) + if not py_path.is_absolute(): + py_path = Path(srcdir) / py_path + if not py_path.exists(): + return [] + try: + tree = ast.parse(py_path.read_text(encoding='utf-8', errors='ignore')) + except SyntaxError: + return [] + seen: set[str] = set() + needed: list[str] = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + roots = {alias.name.split('.')[0] for alias in node.names} + elif isinstance(node, ast.ImportFrom) and node.module: + roots = {node.module.split('.')[0]} + else: + continue + for root in roots: + if (pkg := _GALLERY_EXTRAS.get(root)) and pkg not in seen: + seen.add(pkg) + needed.append(pkg) + return needed gen_rst.EXAMPLE_HEADER = """ @@ -571,6 +622,38 @@ def qt_docstrings(app, what, name, obj, options, lines): # -- Docs build setup ------------------------------------------------------ +def add_gallery_extra_deps_notice(app, docname, source): + """Inject an extra-dependencies notice beneath each gallery example title. + + Only fires for examples that import packages beyond ``napari[all]``. + The packages are detected by scanning the source ``.py`` file via AST, + comparing against :data:`_GALLERY_EXTRAS`. + """ + if not docname.startswith('gallery/') or docname.endswith('/index'): + return + content = source[0] + if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: + return + extra_deps = _get_example_extra_deps(content, app.srcdir) + if not extra_deps: + return + title_match = _GALLERY_TITLE_RE.search(content) + if title_match is None: + return + deps_str = ' '.join(f'``{d}``' for d in extra_deps) + install_str = ' '.join(extra_deps) + notice = f""" +.. only:: html + + .. container:: napari-gallery-extra-deps + + **Extra dependencies required:** {deps_str} + — install with: ``pip install {install_str}`` + +""" + source[0] = content[: title_match.end()] + notice + content[title_match.end() :] + + def setup(app): """Set up docs build. @@ -585,6 +668,7 @@ def setup(app): app.registry.source_suffix.pop('.ipynb', None) app.connect('source-read', add_google_calendar_secrets) app.connect('source-read', add_gallery_download_buttons) + app.connect('source-read', add_gallery_extra_deps_notice) app.connect('linkcheck-process-uri', rewrite_github_anchor) app.connect('autodoc-process-docstring', qt_docstrings) From 8d3cd9aedff282264738d4f34a3651170b7bb421 Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 19 May 2026 14:01:48 -0500 Subject: [PATCH 05/12] abstraction and addition of dep check --- docs/conf.py | 251 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 150 insertions(+), 101 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index dc55bd919..491f887f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,19 +10,27 @@ # -- Imports -------------------------------------------------------------- import ast -import logging import json +import logging import os import re +import tomllib from datetime import datetime +from functools import cache from importlib import import_module -from importlib.metadata import distribution, version as metadata_version +from importlib.metadata import ( + distribution, + packages_distributions, + version as metadata_version, +) from pathlib import Path from urllib.parse import urlparse, urlunparse import napari from jinja2.filters import FILTERS from napari._version import __version_tuple__ +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name from packaging.version import parse as parse_version from pygments.lexers.configs import TOMLLexer from sphinx.highlighting import lexers @@ -397,86 +405,102 @@ def napari_scraper(block, block_vars, gallery_conf): return scrapers.figure_rst(img_paths, gallery_conf['src_dir']) +GALLERY_SOURCE_PATH_RE = re.compile(r'\.\. "(.+\.py)"\n') GALLERY_TITLE_RE = re.compile(r'^.+\n=+\n', re.MULTILINE) - - -def add_gallery_download_buttons(app, docname, source): - """Add compact top-of-page download links to generated gallery examples. - - The title of the example is used to determine where to insert the download links, - which are only added to generated gallery examples. - The links are added after the title. - """ - if not docname.startswith('gallery/'): - return - if docname.endswith('/index') or docname.endswith('sg_execution_times'): - return - - content = source[0] - if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: - return - - example_name = Path(docname).name - title_match = GALLERY_TITLE_RE.search(content) - if title_match is None: - return - - download_block = f""" -.. only:: html - - .. container:: napari-gallery-downloads - - .. container:: sphx-glr-download - - :download:`Python (.py) <{example_name}.py>` - - .. container:: sphx-glr-download - - :download:`Notebook (.ipynb) <{example_name}.ipynb>` -""" - source[0] = ( - content[: title_match.end()] - + download_block - + content[title_match.end() :] +NAPARI_PYPROJECT = Path(__file__).resolve().parents[2] / 'napari' / 'pyproject.toml' +PLUGIN_INSTALL_GUIDE = '../plugins/start_using_plugins/finding_and_installing_plugins' +GALLERY_IGNORED_PACKAGES = frozenset({'pooch'}) + + +def _marker_matches(requirement: Requirement) -> bool: + return requirement.marker is None or requirement.marker.evaluate() + + +@cache +def _load_napari_pyproject() -> dict: + with NAPARI_PYPROJECT.open('rb') as file: + return tomllib.load(file) + + +@cache +def _get_napari_declared_packages(extra: str | None = None) -> frozenset[str]: + project = _load_napari_pyproject()['project'] + packages = { + canonicalize_name(requirement.name) + for requirement in map(Requirement, project['dependencies']) + if _marker_matches(requirement) + } + if extra is None: + return frozenset(packages) + + pending = [extra] + seen_extras = set() + optional_dependencies = project['optional-dependencies'] + while pending: + current_extra = pending.pop() + if current_extra in seen_extras: + continue + seen_extras.add(current_extra) + for requirement in map(Requirement, optional_dependencies[current_extra]): + if not _marker_matches(requirement): + continue + if canonicalize_name(requirement.name) == 'napari' and requirement.extras: + pending.extend(sorted(requirement.extras)) + continue + packages.add(canonicalize_name(requirement.name)) + + return frozenset(packages) + + +@cache +def _get_gallery_extra_packages() -> frozenset[str]: + optional_dependencies = _load_napari_pyproject()['project']['optional-dependencies'] + gallery_packages = { + canonicalize_name(requirement.name) + for requirement in map(Requirement, optional_dependencies['gallery']) + if _marker_matches(requirement) + } + return frozenset( + sorted( + gallery_packages + - _get_napari_declared_packages('all') + - GALLERY_IGNORED_PACKAGES + ) ) -# --------------------------------------------------------------------------- -# Gallery extra-dependency detection -# --------------------------------------------------------------------------- - -# Packages from napari's [dependency-groups] gallery that are NOT included in -# a standard `napari[all]` install. Maps import-root -> pip package name. -_GALLERY_EXTRAS: dict[str, str] = { - 'glasbey': 'glasbey', - 'matplotlib': 'matplotlib', - 'meshio': 'meshio', - 'nilearn': 'nilearn', - 'trackastra': 'trackastra', - 'xarray': 'xarray', -} -# Matches the source .py path embedded in every sphinx-gallery RST header. -_GALLERY_SOURCE_PATH_RE = re.compile(r'\.\. "(.+\.py)"\n') -# Matches the first top-level RST title (text + === underline). -_GALLERY_TITLE_RE = re.compile(r'^.+\n=+\n', re.MULTILINE) +@cache +def _get_gallery_extra_import_roots() -> dict[str, str]: + gallery_packages = _get_gallery_extra_packages() + import_roots: dict[str, str] = {} + for module_name, distributions in packages_distributions().items(): + for dist_name in distributions: + package_name = canonicalize_name(dist_name) + if package_name in gallery_packages: + import_roots[module_name] = package_name + break + return import_roots def _get_example_extra_deps(content: str, srcdir: str) -> list[str]: - """Return extra pip packages needed by a gallery example beyond napari[all].""" - path_match = _GALLERY_SOURCE_PATH_RE.search(content) + path_match = GALLERY_SOURCE_PATH_RE.search(content) if path_match is None: return [] + py_path = Path(path_match.group(1)) if not py_path.is_absolute(): py_path = Path(srcdir) / py_path if not py_path.exists(): return [] + try: tree = ast.parse(py_path.read_text(encoding='utf-8', errors='ignore')) except SyntaxError: return [] + + import_roots = _get_gallery_extra_import_roots() + extra_deps: list[str] = [] seen: set[str] = set() - needed: list[str] = [] for node in ast.walk(tree): if isinstance(node, ast.Import): roots = {alias.name.split('.')[0] for alias in node.names} @@ -484,11 +508,69 @@ def _get_example_extra_deps(content: str, srcdir: str) -> list[str]: roots = {node.module.split('.')[0]} else: continue + for root in roots: - if (pkg := _GALLERY_EXTRAS.get(root)) and pkg not in seen: - seen.add(pkg) - needed.append(pkg) - return needed + package_name = import_roots.get(root) + if package_name and package_name not in seen: + seen.add(package_name) + extra_deps.append(package_name) + + return extra_deps + + +def _gallery_download_block(example_name: str) -> str: + return f""" +.. only:: html + + .. container:: napari-gallery-downloads + + .. container:: sphx-glr-download + + :download:`Python (.py) <{example_name}.py>` + + .. container:: sphx-glr-download + + :download:`Notebook (.ipynb) <{example_name}.ipynb>` +""" + + +def _gallery_extra_deps_note(extra_deps: list[str]) -> str: + deps = ', '.join(f'``{package}``' for package in extra_deps) + return f""" +.. admonition:: Extra packages required + +This example requires additional packages that are not available in typical +napari installations. For this example to work, you will need to install: {deps}. +See :doc:`the installation guide <{PLUGIN_INSTALL_GUIDE}>` for ways to install +additional packages, including from within napari. +""" + + +def augment_gallery_example(app, docname, source): + """Add shared top-of-page UI for generated gallery example pages.""" + if not docname.startswith('gallery/'): + return + if docname.endswith('/index') or docname.endswith('sg_execution_times'): + return + + content = source[0] + if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: + return + + title_match = GALLERY_TITLE_RE.search(content) + if title_match is None: + return + + blocks = [_gallery_download_block(Path(docname).name)] + extra_deps = _get_example_extra_deps(content, app.srcdir) + if extra_deps: + blocks.append(_gallery_extra_deps_note(extra_deps)) + + source[0] = ( + content[: title_match.end()] + + '\n'.join(blocks) + + content[title_match.end() :] + ) gen_rst.EXAMPLE_HEADER = """ @@ -622,38 +704,6 @@ def qt_docstrings(app, what, name, obj, options, lines): # -- Docs build setup ------------------------------------------------------ -def add_gallery_extra_deps_notice(app, docname, source): - """Inject an extra-dependencies notice beneath each gallery example title. - - Only fires for examples that import packages beyond ``napari[all]``. - The packages are detected by scanning the source ``.py`` file via AST, - comparing against :data:`_GALLERY_EXTRAS`. - """ - if not docname.startswith('gallery/') or docname.endswith('/index'): - return - content = source[0] - if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: - return - extra_deps = _get_example_extra_deps(content, app.srcdir) - if not extra_deps: - return - title_match = _GALLERY_TITLE_RE.search(content) - if title_match is None: - return - deps_str = ' '.join(f'``{d}``' for d in extra_deps) - install_str = ' '.join(extra_deps) - notice = f""" -.. only:: html - - .. container:: napari-gallery-extra-deps - - **Extra dependencies required:** {deps_str} - — install with: ``pip install {install_str}`` - -""" - source[0] = content[: title_match.end()] + notice + content[title_match.end() :] - - def setup(app): """Set up docs build. @@ -667,8 +717,7 @@ def setup(app): """ app.registry.source_suffix.pop('.ipynb', None) app.connect('source-read', add_google_calendar_secrets) - app.connect('source-read', add_gallery_download_buttons) - app.connect('source-read', add_gallery_extra_deps_notice) + app.connect('source-read', augment_gallery_example) app.connect('linkcheck-process-uri', rewrite_github_anchor) app.connect('autodoc-process-docstring', qt_docstrings) From 377563f14faf16ea555293fabed342ea15186eb8 Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 19 May 2026 14:10:21 -0500 Subject: [PATCH 06/12] fix deps admonition --- docs/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 491f887f3..a83a74799 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -539,10 +539,10 @@ def _gallery_extra_deps_note(extra_deps: list[str]) -> str: return f""" .. admonition:: Extra packages required -This example requires additional packages that are not available in typical -napari installations. For this example to work, you will need to install: {deps}. -See :doc:`the installation guide <{PLUGIN_INSTALL_GUIDE}>` for ways to install -additional packages, including from within napari. + This example requires additional packages that are not available in typical + napari installations. For this example to work, you will need to install: {deps}. + See :doc:`the installation guide <{PLUGIN_INSTALL_GUIDE}>` for ways to install + additional packages, including from within napari. """ From 80e2ab2d9f8a987de81121052032429a7cc4ce92 Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 19 May 2026 14:38:01 -0500 Subject: [PATCH 07/12] strengthen deps admonition help --- docs/conf.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a83a74799..64547dbe3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -409,6 +409,7 @@ def napari_scraper(block, block_vars, gallery_conf): GALLERY_TITLE_RE = re.compile(r'^.+\n=+\n', re.MULTILINE) NAPARI_PYPROJECT = Path(__file__).resolve().parents[2] / 'napari' / 'pyproject.toml' PLUGIN_INSTALL_GUIDE = '../plugins/start_using_plugins/finding_and_installing_plugins' +NAPARI_INSTALL_GUIDE = '../getting_started/installation' GALLERY_IGNORED_PACKAGES = frozenset({'pooch'}) @@ -540,9 +541,11 @@ def _gallery_extra_deps_note(extra_deps: list[str]) -> str: .. admonition:: Extra packages required This example requires additional packages that are not available in typical - napari installations. For this example to work, you will need to install: {deps}. - See :doc:`the installation guide <{PLUGIN_INSTALL_GUIDE}>` for ways to install - additional packages, including from within napari. + napari installations. + For this example to work in :doc:`recommended napari installations <{NAPARI_INSTALL_GUIDE}>`, + you will need to install additional packages: {deps}. + See the :doc:`the plugin installation guide <{PLUGIN_INSTALL_GUIDE}>` + for ways to install additional packages from within napari. """ From ac55171be973e322daa63a85ce998dc8acf34daf Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Wed, 20 May 2026 15:18:32 -0500 Subject: [PATCH 08/12] sync css with #1021 --- docs/_static/custom.css | 140 ++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 0eed907b2..0070f87e3 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,60 +1,45 @@ -/* -Sphinx-Gallery has compatible CSS to fix default sphinx themes -Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc, -scrolls, agogo, traditional, nature, haiku, pyramid -Tested for Read the Docs theme 0.1.7 */ - -html[data-theme="light"] div.sphx-glr-download a { - background-color: rgb(255, 255, 255) !important; - background-image: linear-gradient(to bottom, rgb(255, 255, 255), #ffffff) !important; +/* ── Buttons for homepage and gallery ───────────────────────────────────── */ + +.button-primary, +.button-alt, +.napari-gallery-downloads .sphx-glr-download a { + background-image: none !important; border-radius: 4px; - border: 1px solid #ffffff !important; - color: #000; - display: inline-block; - font-weight: bold; - padding: 1ex; text-align: center; + text-decoration: none; } -html[data-theme="light"] div.sphx-glr-download a:hover { - 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; - background-color: #ffffff !important; +/* makes homepage button take up width of column */ +.button-primary { + display: block; } -html[data-theme="dark"] div.sphx-glr-download a { - background-color: #222832 !important; - background-image: linear-gradient(to bottom, #222832, #222832) !important; - border-radius: 4px; - border: 1px solid #222832 !important; - color: #000; +/* Allow border override to show through on example buttons */ +.napari-gallery-downloads .sphx-glr-download a { + border: 1px solid transparent; display: inline-block; - font-weight: bold; - padding: 1ex; - text-align: center; + padding: 0.45rem 0.8rem; } -html[data-theme="dark"] div.sphx-glr-download a:hover { - 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; - background-color: #222832 !important; +/* force sphinx-gallery download code to inherit text color */ +.napari-gallery-downloads .sphx-glr-download a code.download { + color: inherit !important; } +/* horizontally align and reduce lower margin of container */ .napari-gallery-downloads { display: flex; flex-wrap: wrap; - gap: 0.5rem; - margin: 0 0 1.25rem; + gap: 0.375rem; + margin: 0 0 0.35rem; } +/* remove spacing around download buttons */ .napari-gallery-downloads .sphx-glr-download { margin: 0; } +/* Remove the sphinx-gallery downloads note and buttons */ .sphx-glr-download-link-note, .sphx-glr-download-jupyter, .sphx-glr-download-python, @@ -62,53 +47,75 @@ html[data-theme="dark"] div.sphx-glr-download a:hover { display: none; } -/* Version warning banner color */ -#bd-header-version-warning { - background-color: color-mix(in srgb, var(--pst-color-secondary-bg), transparent 30%); +html[data-theme="light"] .button-primary, +html[data-theme="light"] .napari-gallery-downloads .sphx-glr-download a { + background-color: var(--napari-primary-blue) !important; + border-color: var(--napari-primary-blue) !important; + color: var(--napari-color-text-base) !important; } -#bd-header-version-warning .pst-button-link-to-stable-version { - background-color: color-mix(in srgb, var(--pst-color-secondary-bg), transparent 0%); - border-color: var(--pst-color-secondary-bg); - color: var(--napari-color-text-base); - font-weight: 700; +html[data-theme="dark"] .button-primary, +html[data-theme="dark"] .napari-gallery-downloads .sphx-glr-download a { + background-color: var(--napari-purple) !important; + border-color: var(--napari-purple) !important; + color: var(--napari-color-text-base) !important; } -#bd-header-version-warning .pst-button-link-to-stable-version:hover { - background-color: var(--pst-color-secondary-bg); - border-color: var(--pst-color-secondary-bg); - color: var(--napari-color-text-base); - font-weight: 700; +html[data-theme="light"] .button-primary:hover, +html[data-theme="light"] .napari-gallery-downloads .sphx-glr-download a:hover { + background-color: color-mix(in srgb, var(--napari-primary-blue), transparent 25%) !important; + text-decoration: underline; } -/* ── Homepage accent cards ─────────────────────────────────────────── - Install, Download cards with theme-aware napari blue. - sphinx-design marks every property !important, so we must too. */ - -.homepage-button { - display: block; +html[data-theme="dark"] .button-primary:hover, +html[data-theme="dark"] .napari-gallery-downloads .sphx-glr-download a:hover { + background-color: color-mix(in srgb, var(--napari-purple), transparent 25%) !important; + text-decoration: underline; } -html[data-theme="light"] .homepage-button { - background-color: var(--napari-primary-blue) !important; +html[data-theme="light"] .button-alt { + background-color: var(--pst-color-background) !important; border-color: var(--napari-primary-blue) !important; color: var(--napari-color-text-base) !important; } -html[data-theme="dark"] .homepage-button { - background-color: var(--napari-purple) !important; +html[data-theme="dark"] .button-alt { + background-color: var(--pst-color-background) !important; border-color: var(--napari-purple) !important; color: var(--napari-color-text-base) !important; } -html[data-theme="light"] .homepage-button:hover { +html[data-theme="light"] .button-alt:hover { + background-color: var(--pst-color-background) !important; + border-color: var(--napari-primary-blue) !important; + color: var(--napari-color-text-base) !important; text-decoration: underline; - background-color: color-mix(in srgb, var(--napari-primary-blue), transparent 25%) !important; } -html[data-theme="dark"] .homepage-button:hover { +html[data-theme="dark"] .button-alt:hover { + background-color: var(--pst-color-background) !important; + border-color: var(--napari-purple) !important; + color: var(--napari-color-text-base) !important; text-decoration: underline; - background-color: color-mix(in srgb, var(--napari-purple), transparent 25%) !important; +} + +/* Version warning banner color */ +#bd-header-version-warning { + 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%); + border-color: var(--pst-color-secondary-bg); + color: var(--napari-color-text-base); + font-weight: 700; +} + +#bd-header-version-warning .pst-button-link-to-stable-version:hover { + background-color: var(--pst-color-secondary-bg); + border-color: var(--pst-color-secondary-bg); + color: var(--napari-color-text-base); + font-weight: 700; } /* ── Homepage quicklinks (icon columns) ──────────────────────────────── */ @@ -241,8 +248,3 @@ html[data-theme="dark"] .homepage-quicklinks a { html[data-theme="dark"] img#homepage-featured-example-image { background-color: var(--pst-color-background) !important; } - -.homepage-example-button:hover { - background-color: var(--pst-color-background) !important; - color: var(--napari-color-text-base) !important; -} \ No newline at end of file From b70ade095b3b42bc3a9ff785314fbb761205908b Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Wed, 20 May 2026 15:21:30 -0500 Subject: [PATCH 09/12] sync index.md --- docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index b775ffabb..aa3935234 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ theme: :color: primary :ref-type: myst :shadow: -:class: homepage-button +:class: button-primary {material-regular}`arrow_forward;1.2em` **Get started** ``` @@ -133,8 +133,8 @@ A standalone installer for when you want napari without setting up Python first. Display an image in napari and explore the viewer with a minimal example. </p> <div class="homepage-featured-example__actions"> - <a class="sd-btn sd-btn-primary sd-shadow-sm homepage-button" href="gallery.html">Examples gallery</a> - <button class="sd-btn sd-btn-outline-primary sd-shadow-sm homepage-example-button" id="homepage-featured-example-reroll" type="button">Show another example</button> + <a class="sd-btn sd-btn-primary sd-shadow-sm button-primary" href="gallery.html">Examples gallery</a> + <button class="sd-btn sd-btn-outline-primary sd-shadow-sm button-alt" id="homepage-featured-example-reroll" type="button">Show another example</button> </div> </div> <a class="homepage-featured-example__media" href="gallery/add_image.html" aria-label="View the featured napari example"> From c7aac0e8acbe5faca128a1b6ea5d2703e0d47791 Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 26 May 2026 23:34:44 -0500 Subject: [PATCH 10/12] sync with #1021 --- docs/conf.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 64547dbe3..665fb6358 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -406,7 +406,9 @@ def napari_scraper(block, block_vars, gallery_conf): GALLERY_SOURCE_PATH_RE = re.compile(r'\.\. "(.+\.py)"\n') -GALLERY_TITLE_RE = re.compile(r'^.+\n=+\n', re.MULTILINE) +GALLERY_METADATA_END_RE = re.compile( + r'^\.\. GENERATED FROM PYTHON SOURCE LINES \d+-\d+\s*$', re.MULTILINE +) NAPARI_PYPROJECT = Path(__file__).resolve().parents[2] / 'napari' / 'pyproject.toml' PLUGIN_INSTALL_GUIDE = '../plugins/start_using_plugins/finding_and_installing_plugins' NAPARI_INSTALL_GUIDE = '../getting_started/installation' @@ -550,7 +552,13 @@ def _gallery_extra_deps_note(extra_deps: list[str]) -> str: def augment_gallery_example(app, docname, source): - """Add shared top-of-page UI for generated gallery example pages.""" + """Add compact download links and admontion before script. + + Sphinx-Gallery emits the example title first, followed by the short + description and tags, and only then the ``GENERATED FROM PYTHON SOURCE`` + marker. Insert the links just before that first marker so they stay below + the description metadata without overriding Sphinx-Gallery templates. + """ if not docname.startswith('gallery/'): return if docname.endswith('/index') or docname.endswith('sg_execution_times'): @@ -560,8 +568,8 @@ def augment_gallery_example(app, docname, source): if 'THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.' not in content: return - title_match = GALLERY_TITLE_RE.search(content) - if title_match is None: + metadata_end_match = GALLERY_METADATA_END_RE.search(content) + if metadata_end_match is None: return blocks = [_gallery_download_block(Path(docname).name)] @@ -570,9 +578,9 @@ def augment_gallery_example(app, docname, source): blocks.append(_gallery_extra_deps_note(extra_deps)) source[0] = ( - content[: title_match.end()] + content[: metadata_end_match.start()] + '\n'.join(blocks) - + content[title_match.end() :] + + content[metadata_end_match.start() :] ) From bc68729dbfac143b79f12f201dc808a95dd79970 Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Tue, 26 May 2026 23:55:21 -0500 Subject: [PATCH 11/12] point install help lnik to napari-plugin-manager docs --- docs/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 665fb6358..49636aa5d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -410,7 +410,9 @@ def napari_scraper(block, block_vars, gallery_conf): r'^\.\. GENERATED FROM PYTHON SOURCE LINES \d+-\d+\s*$', re.MULTILINE ) NAPARI_PYPROJECT = Path(__file__).resolve().parents[2] / 'napari' / 'pyproject.toml' -PLUGIN_INSTALL_GUIDE = '../plugins/start_using_plugins/finding_and_installing_plugins' +PLUGIN_MANAGER_ADDITIONAL_PACKAGES_GUIDE = ( + 'https://napari.org/napari-plugin-manager/#installing-via-direct-entry' +) NAPARI_INSTALL_GUIDE = '../getting_started/installation' GALLERY_IGNORED_PACKAGES = frozenset({'pooch'}) @@ -546,7 +548,7 @@ def _gallery_extra_deps_note(extra_deps: list[str]) -> str: napari installations. For this example to work in :doc:`recommended napari installations <{NAPARI_INSTALL_GUIDE}>`, you will need to install additional packages: {deps}. - See the :doc:`the plugin installation guide <{PLUGIN_INSTALL_GUIDE}>` + See the `plugin manager guide on installing additional packages <{PLUGIN_MANAGER_ADDITIONAL_PACKAGES_GUIDE}>` for ways to install additional packages from within napari. """ From 6153ed6b0367347bf08e85be928a6a3891bf86cc Mon Sep 17 00:00:00 2001 From: Tim Monko <timmonko@gmail.com> Date: Wed, 27 May 2026 00:01:18 -0500 Subject: [PATCH 12/12] fix link to plugin manager guide rendering --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 49636aa5d..2c7bd2e50 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -548,7 +548,7 @@ def _gallery_extra_deps_note(extra_deps: list[str]) -> str: napari installations. For this example to work in :doc:`recommended napari installations <{NAPARI_INSTALL_GUIDE}>`, you will need to install additional packages: {deps}. - See the `plugin manager guide on installing additional packages <{PLUGIN_MANAGER_ADDITIONAL_PACKAGES_GUIDE}>` + See the `plugin manager guide <{PLUGIN_MANAGER_ADDITIONAL_PACKAGES_GUIDE}>`_ for ways to install additional packages from within napari. """