From 49f5845893cc2702b70585c6eec2f493df644b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Mon, 27 Oct 2025 13:24:11 +0900 Subject: [PATCH 1/5] Add mdformat to pre-commit config --- .pre-commit-config.yaml | 7 + .ruff.toml | 71 ++++++++ docs/conf.py | 382 +++++++++++++++++++--------------------- 3 files changed, 263 insertions(+), 197 deletions(-) create mode 100644 .ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2a2f5248..a166288f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,5 +30,12 @@ repos: - id: ruff-format types_or: [ python, pyi ] +- repo: https://github.com/executablebooks/mdformat + rev: 0.7.22 + hooks: + - id: mdformat + additional_dependencies: [mdformat-myst, mdformat-ruff] + files: (docs/.) + default_language_version: python: python3.13 diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 000000000..a49653dc2 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,71 @@ +fix = true + +exclude = [ + "docs/conf.py", + "docs/_scripts/*.py", + "docs/naps/*.md", +] + +[format] +quote-style = "single" + +[lint] +select = [ + "E", "F", "W", #flake8 + "UP", # pyupgrade + "I", # isort + "YTT", #flake8-2020 + "TC", # flake8-type-checing + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "COM", # flake8-commas + "SIM", # flake8-simplify + "INP", # flake8-no-pep420 + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "TID", # flake8-tidy-imports # replace absolutify import + "TRY", # tryceratops + "ICN", # flake8-import-conventions + "RUF", # ruff specyfic rules + "NPY201", # checks compatibility with numpy version 2.0 + "ASYNC", # flake8-async + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "LOG", # flake8-logging + "SLOT", # flake8-slots + "PT", # flake8-pytest-style + "T20", # flake8-print +] +ignore = [ + "E501", "TC001", "TC002", "TC003", + "A003", # flake8-builtins - we have class attributes violating these rule + "COM812", # flake8-commas - we don't like adding comma on single line of arguments + "COM819", # conflicts with ruff-format + "RET504", # not fixed yet https://github.com/charliermarsh/ruff/issues/2950 + "TRY003", # require implement multiple exception class + "RUF005", # problem with numpy compatybility, see https://github.com/charliermarsh/ruff/issues/2142#issuecomment-1451038741 + "B028", # need to be fixed + "PYI015", # it produces bad looking files (@jni opinion) + "W191", "Q000", "Q001", "Q002", "Q003", "ISC001", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "UP007", # temporary disable migrtion form Union[X, Y] to X | Y + "UP045", # temporary disable migrtion from Optional[X] to X | None + "TC006", # put types in quotes in typing.cast +] + +[lint.flake8-builtins] +builtins-allowed-modules = ["io", "types", "threading"] + +[lint.pyupgrade] +keep-runtime-typing = true + +[lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "single" +multiline-quotes = "double" diff --git a/docs/conf.py b/docs/conf.py index 2764a3ad9..82cef1b9f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,223 +18,215 @@ 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.version import parse as parse_version from pygments.lexers.configs import TOMLLexer -from sphinx_gallery import gen_rst -from sphinx_gallery import scrapers -from sphinx_gallery.sorting import ExampleTitleSortKey from sphinx.highlighting import lexers from sphinx.util import logging as sphinx_logging - -import napari -from napari._version import __version_tuple__ +from sphinx_gallery import gen_rst, scrapers +from sphinx_gallery.sorting import ExampleTitleSortKey # -- Version information --------------------------------------------------- napari_version = parse_version(napari.__version__) release = str(napari_version) -if napari_version.is_devrelease: - version = "dev" -else: - version = napari_version.base_version +version = 'dev' if napari_version.is_devrelease else napari_version.base_version # -- Project information --------------------------------------------------- -project = "napari" -copyright = f"{datetime.now().year}, The napari team" -author = "The napari team" +project = 'napari' +copyright = f'{datetime.now().year}, The napari team' +author = 'The napari team' # -- Sphinx extensions ------------------------------------------------------- extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.intersphinx", - "sphinx.ext.napoleon", - "sphinx.ext.viewcode", - "sphinxcontrib.mermaid", - "sphinx_copybutton", - "sphinx_design", - "sphinx_external_toc", - "sphinx_favicon", - "sphinx_gallery.gen_gallery", - "sphinx_tags", - "myst_nb", - "sphinxext.opengraph", + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', + 'sphinxcontrib.mermaid', + 'sphinx_copybutton', + 'sphinx_design', + 'sphinx_external_toc', + 'sphinx_favicon', + 'sphinx_gallery.gen_gallery', + 'sphinx_tags', + 'myst_nb', + 'sphinxext.opengraph', ] # -- HTML Theme ------------------------------------------------------------ # See https://github.com/napari/napari-sphinx-theme for more information. -html_theme = "napari_sphinx_theme" -html_title = "napari" -html_sourcelink_suffix = "" +html_theme = 'napari_sphinx_theme' +html_title = 'napari' +html_sourcelink_suffix = '' # Check version and set version_match which is used by the version switcher -if version == "dev": - version_match = "dev" -else: - version_match = str(release) +version_match = 'dev' if version == 'dev' else str(release) # Define the json_url for our version switcher. When developing locally, you # can change this to match the local version i.e. "_static/version_switcher.json" # However, this must be the absolute URL for deployed versions. See # https://github.com/napari/napari.github.io/issues/427 for details. -json_url = "https://napari.org/dev/_static/version_switcher.json" +json_url = 'https://napari.org/dev/_static/version_switcher.json' # Path to static files, images, favicons, logos, css, and extra templates -html_static_path = ["_static"] -html_logo = "_static/images/logo.png" +html_static_path = ['_static'] +html_logo = '_static/images/logo.png' html_css_files = [ - "custom.css", + 'custom.css', ] -templates_path = ["_templates"] +templates_path = ['_templates'] favicons = [ { # the SVG is the "best" and contains code to detect OS light/dark mode - "static-file": "favicon/logo-silhouette-dark-light.svg", - "type": "image/svg+xml", + 'static-file': 'favicon/logo-silhouette-dark-light.svg', + 'type': 'image/svg+xml', }, { # Safari in Oct. 2022 does not support SVG # an ICO would work as well, but PNG should be just as good # setting sizes="any" is needed for Chrome to prefer the SVG - "sizes": "any", - "static-file": "favicon/logo-silhouette-192.png", + 'sizes': 'any', + 'static-file': 'favicon/logo-silhouette-192.png', }, { # this is used on iPad/iPhone for "Save to Home Screen" # apparently some other apps use it as well - "rel": "apple-touch-icon", - "sizes": "180x180", - "static-file": "favicon/logo-noborder-180.png", + 'rel': 'apple-touch-icon', + 'sizes': '180x180', + 'static-file': 'favicon/logo-noborder-180.png', }, ] # napari sphinx theme options html_theme_options = { - "external_links": [ - {"name": "napari hub", "url": "https://napari-hub.org"}, + 'external_links': [ + {'name': 'napari hub', 'url': 'https://napari-hub.org'}, ], - "github_url": "https://github.com/napari/napari", - "navbar_start": ["navbar-logo", "navbar-project"], - "navbar_end": ["version-switcher", "navbar-icon-links", "theme-switcher"], - "switcher": { - "json_url": json_url, - "version_match": version_match, + 'github_url': 'https://github.com/napari/napari', + 'navbar_start': ['navbar-logo', 'navbar-project'], + 'navbar_end': ['version-switcher', 'navbar-icon-links', 'theme-switcher'], + 'switcher': { + 'json_url': json_url, + 'version_match': version_match, }, - "show_version_warning_banner": True, - "navbar_persistent": [], - "header_links_before_dropdown": 6, - "secondary_sidebar_items": ["page-toc"], - "pygments_light_style": "napari", - "pygments_dark_style": "dracula", - "announcement": "", - "back_to_top_button": False, - "analytics": { + 'show_version_warning_banner': True, + 'navbar_persistent': [], + 'header_links_before_dropdown': 6, + 'secondary_sidebar_items': ['page-toc'], + 'pygments_light_style': 'napari', + 'pygments_dark_style': 'dracula', + 'announcement': '', + 'back_to_top_button': False, + 'analytics': { # The domain you'd like to use for this analytics instance - "plausible_analytics_domain": "napari.org", + 'plausible_analytics_domain': 'napari.org', # The analytics script that is served by Plausible - "plausible_analytics_url": "https://plausible.io/js/plausible.js", + 'plausible_analytics_url': 'https://plausible.io/js/plausible.js', }, - "footer_start": ["napari-footer-links"], - "footer_end": ["napari-copyright"], + 'footer_start': ['napari-footer-links'], + 'footer_end': ['napari-copyright'], } # sidebar content html_sidebars = { - "**": ["search-field.html", "sidebar-nav-bs"], - "index": ["search-field.html", "sidebar-link-items.html"], + '**': ['search-field.html', 'sidebar-nav-bs'], + 'index': ['search-field.html', 'sidebar-link-items.html'], } -# html context is passed into the template engine’s context for all pages. +# html context is passed into the template engine's context for all pages. html_context = { # use Light theme only, don't auto switch (default) - "default_mode": "auto", + 'default_mode': 'auto', # add release version to context - "release": release, - "version": version, + 'release': release, + 'version': version, } # intersphinx configuration for frequently used links to other projects intersphinx_mapping = { - "python": ["https://docs.python.org/3", None], - "numpy": ["https://numpy.org/doc/stable/", None], - "napari_plugin_engine": [ - "https://napari-plugin-engine.readthedocs.io/en/latest/", - "https://napari-plugin-engine.readthedocs.io/en/latest/objects.inv", + 'python': ['https://docs.python.org/3', None], + 'numpy': ['https://numpy.org/doc/stable/', None], + 'napari_plugin_engine': [ + 'https://napari-plugin-engine.readthedocs.io/en/latest/', + 'https://napari-plugin-engine.readthedocs.io/en/latest/objects.inv', ], - "magicgui": [ - "https://pyapp-kit.github.io/magicgui/", - "https://pyapp-kit.github.io/magicgui/objects.inv", + 'magicgui': [ + 'https://pyapp-kit.github.io/magicgui/', + 'https://pyapp-kit.github.io/magicgui/objects.inv', ], - "app-model": [ - "https://app-model.readthedocs.io/en/latest/", - "https://app-model.readthedocs.io/en/latest/objects.inv", + 'app-model': [ + 'https://app-model.readthedocs.io/en/latest/', + 'https://app-model.readthedocs.io/en/latest/objects.inv', ], - "vispy": [ - "https://vispy.org/", - "https://vispy.org/objects.inv", + 'vispy': [ + 'https://vispy.org/', + 'https://vispy.org/objects.inv', ], } # myst markdown extensions for additional markdown features myst_enable_extensions = [ - "colon_fence", - "dollarmath", - "substitution", - "tasklist", - "attrs_inline", - "linkify", + 'colon_fence', + 'dollarmath', + 'substitution', + 'tasklist', + 'attrs_inline', + 'linkify', ] myst_footnote_transition = False myst_heading_anchors = 4 # configuration settings for sphinx and its extensions -comments_config = {"hypothesis": False, "utterances": False} -external_toc_path = "_toc.yml" +comments_config = {'hypothesis': False, 'utterances': False} +external_toc_path = '_toc.yml' external_toc_exclude_missing = False -lexers["toml"] = TOMLLexer(startinline=True) -napoleon_custom_sections = [("Events", "params_style")] +lexers['toml'] = TOMLLexer(startinline=True) +napoleon_custom_sections = [('Events', 'params_style')] # use an env var to control whether noteboos are executed -nb_execution_mode = os.environ.get("NB_EXECUTION_MODE", "auto") -nb_output_stderr = "show" +nb_execution_mode = os.environ.get('NB_EXECUTION_MODE', 'auto') +nb_output_stderr = 'show' mermaid_d3_zoom = True -mermaid_version = "11.4.1" -mermaid_include_elk = "" +mermaid_version = '11.4.1' +mermaid_include_elk = '' tags_create_tags = True -tags_output_dir = "_tags" -tags_overview_title = "Tags" -tags_extension = ["md", "rst"] +tags_output_dir = '_tags' +tags_overview_title = 'Tags' +tags_extension = ['md', 'rst'] panels_add_bootstrap_css = False -pygments_style = "solarized-dark" -suppress_warnings = ["myst.header", "etoc.toctree", "config.cache"] +pygments_style = 'solarized-dark' +suppress_warnings = ['myst.header', 'etoc.toctree', 'config.cache'] # OpenGraph configuration for link previews -ogp_site_url = "https://napari.org/" -ogp_image = "dev/_static/opengraph_image.png" +ogp_site_url = 'https://napari.org/' +ogp_image = 'dev/_static/opengraph_image.png' ogp_use_first_image = False ogp_description_length = 300 -ogp_type = "website" -ogp_site_name = "napari" -ogp_canonical_url = "https://napari.org/stable" +ogp_type = 'website' +ogp_site_name = 'napari' +ogp_canonical_url = 'https://napari.org/stable' ogp_social_cards = { - "image": "_static/logo.png", + 'image': '_static/logo.png', } # glob-style patterns to exclude from docs build source files exclude_patterns = [ - "_build", - "Thumbs.db", - ".DS_Store", - ".jupyter_cache", - "jupyter_execute", - "plugins/_*.md", - "plugins/building_a_plugin/_layer_data_guide.md", - "gallery/index.rst", - "_scripts/README.md", + '_build', + 'Thumbs.db', + '.DS_Store', + '.jupyter_cache', + 'jupyter_execute', + 'plugins/_*.md', + 'plugins/building_a_plugin/_layer_data_guide.md', + 'gallery/index.rst', + '_scripts/README.md', ] # -- Versions and switcher ------------------------------------------------- @@ -249,32 +241,32 @@ def get_supported_python_versions(project_name): classifiers = [ value for key, value in dist.metadata.items() - if key == "Classifier" and value.startswith("Programming Language :: Python ::") + if key == 'Classifier' and value.startswith('Programming Language :: Python ::') ] return [ - parse_version(c.split(" :: ")[-1]) + parse_version(c.split(' :: ')[-1]) for c in classifiers - if not c.endswith("Only") + if not c.endswith('Only') ] -napari_supported_python_versions = get_supported_python_versions("napari") +napari_supported_python_versions = get_supported_python_versions('napari') min_python_version = min(napari_supported_python_versions) max_python_version = max(napari_supported_python_versions) -version_string = ".".join(str(x) for x in __version_tuple__[:3]) +version_string = '.'.join(str(x) for x in __version_tuple__[:3]) # when updating the version below, ensure to also update napari/napari README -python_version = "3.11" -python_version_range = f"{min_python_version}-{max_python_version}" +python_version = '3.11' +python_version_range = f'{min_python_version}-{max_python_version}' myst_substitutions = { - "napari_conda_version": f"`napari={version_string}`", - "napari_version": version_string, - "python_version": python_version, - "python_version_range": python_version_range, - "python_version_code": f"`python={python_version}`", - "conda_create_env": f"```sh\nconda create -y -n napari-env -c conda-forge python={python_version}\nconda activate napari-env\n```", + 'napari_conda_version': f'`napari={version_string}`', + 'napari_version': version_string, + 'python_version': python_version, + 'python_version_range': python_version_range, + 'python_version_code': f'`python={python_version}`', + 'conda_create_env': f'```sh\nconda create -y -n napari-env -c conda-forge python={python_version}\nconda activate napari-env\n```', } # -- Autosummary ------------------------------------------------------------ @@ -291,16 +283,15 @@ def get_attributes(item, obj, modulename): """ module = import_module(modulename) - if hasattr(module, "__all__") and obj not in module.__all__: - return "" + if hasattr(module, '__all__') and obj not in module.__all__: + return '' if hasattr(getattr(module, obj), item): - return f"~{obj}.{item}" - else: - return "" + return f'~{obj}.{item}' + return '' -FILTERS["get_attributes"] = get_attributes +FILTERS['get_attributes'] = get_attributes class FilterSphinxWarnings(logging.Filter): @@ -320,11 +311,9 @@ def __init__(self, app): def filter(self, record: logging.LogRecord) -> bool: msg = record.getMessage() - filter_out = ("duplicate object description",) + filter_out = ('duplicate object description',) - if msg.strip().startswith(filter_out): - return False - return True + return not msg.strip().startswith(filter_out) # -- Examples gallery ------------------------------------------------------- @@ -335,7 +324,7 @@ def reset_napari(gallery_conf, fname): from qtpy.QtWidgets import QApplication settings = get_settings() - settings.appearance.theme = "dark" + settings.appearance.theme = 'dark' # Disabling `QApplication.exec_` means example scripts can call `exec_` # (scripts work when run normally) without blocking example execution by @@ -350,12 +339,12 @@ def napari_scraper(block, block_vars, gallery_conf): `app.processEvents()` allows Qt events to propagateo and prevents hanging. """ - imgpath_iter = block_vars["image_path_iterator"] + imgpath_iter = block_vars['image_path_iterator'] if app := napari.qt.get_qapp(): app.processEvents() else: - return "" + return '' img_paths = [] for win, img_path in zip( @@ -368,7 +357,7 @@ def napari_scraper(block, block_vars, gallery_conf): napari.Viewer.close_all() app.processEvents() - return scrapers.figure_rst(img_paths, gallery_conf["src_dir"]) + return scrapers.figure_rst(img_paths, gallery_conf['src_dir']) gen_rst.EXAMPLE_HEADER = """ @@ -396,29 +385,29 @@ def napari_scraper(block, block_vars, gallery_conf): sphinx_gallery_conf = { # path to your example scripts (this value is set in the Makefile) # 'examples_dirs': '../../napari/examples', - "gallery_dirs": "gallery", # path to where to save gallery generated output - "filename_pattern": "/*.py", - "ignore_pattern": "README.rst|/*_.py", - "default_thumb_file": Path(__file__).parent / "_static" / "images" / "logo.png", - "plot_gallery": "'True'", # https://github.com/sphinx-gallery/sphinx-gallery/pull/304/files - "download_all_examples": False, - "min_reported_time": 10, - "only_warn_on_example_error": False, - "abort_on_example_error": True, - "image_scrapers": ( - "matplotlib", + 'gallery_dirs': 'gallery', # path to where to save gallery generated output + 'filename_pattern': '/*.py', + 'ignore_pattern': 'README.rst|/*_.py', + 'default_thumb_file': Path(__file__).parent / '_static' / 'images' / 'logo.png', + 'plot_gallery': "'True'", # https://github.com/sphinx-gallery/sphinx-gallery/pull/304/files + 'download_all_examples': False, + 'min_reported_time': 10, + 'only_warn_on_example_error': False, + 'abort_on_example_error': True, + 'image_scrapers': ( + 'matplotlib', napari_scraper, ), - "reset_modules": (reset_napari,), - "reference_url": {"napari": None}, - "within_subsection_order": ExampleTitleSortKey, + 'reset_modules': (reset_napari,), + 'reference_url': {'napari': None}, + 'within_subsection_order': ExampleTitleSortKey, } # -- Calendar --------------------------------------------------------------- # We host a google calendar on the docs website. To keep it up to date, we # need an api key to make requests to the Google API for updating calendar events. -GOOGLE_CALENDAR_API_KEY = os.environ.get("GOOGLE_CALENDAR_API_KEY", "") +GOOGLE_CALENDAR_API_KEY = os.environ.get('GOOGLE_CALENDAR_API_KEY', '') def add_google_calendar_secrets(app, docname, source): @@ -428,29 +417,29 @@ def add_google_calendar_secrets(app, docname, source): source file. You can process the contents and replace this item to implement source-level transformations. """ - if docname == "community/meeting_schedule": - source[0] = source[0].replace("{API_KEY}", GOOGLE_CALENDAR_API_KEY) + if docname == 'community/meeting_schedule': + source[0] = source[0].replace('{API_KEY}', GOOGLE_CALENDAR_API_KEY) # -- Links and checks ------------------------------------------------------ linkcheck_allowed_redirects = { - r"https://youtu\.be/.*": r"https://www\.youtube\.com/.*", - r"https://github\.com/napari/napari/releases/download/.*": r"https://objects\.githubusercontent\.com/.*", + r'https://youtu\.be/.*': r'https://www\.youtube\.com/.*', + r'https://github\.com/napari/napari/releases/download/.*': r'https://objects\.githubusercontent\.com/.*', } -linkcheck_anchors_ignore = [r"^!", r"L\d+-L\d+", r"r\d+", r"issuecomment-\d+"] +linkcheck_anchors_ignore = [r'^!', r'L\d+-L\d+', r'r\d+', r'issuecomment-\d+'] linkcheck_ignore = [ - "https://napari.zulipchat.com/", - "../_tags", - "https://en.wikipedia.org/wiki/Napari#/media/File:Tabuaeran_Kiribati.jpg", - "http://localhost:8000", - "https://datadryad.org/stash/downloads/file_stream/182482", - "https://github.com/napari/docs/issues/new/choose", - "https://github.com/napari/napari/issues/new/choose", - "https://github.com/napari/napari/issues/new", - "https://napari-hub.org", - "https://github.com/napari/napari/releases/latest", - "https://onlinelibrary.wiley.com/doi/10.1002/col.20327", + 'https://napari.zulipchat.com/', + '../_tags', + 'https://en.wikipedia.org/wiki/Napari#/media/File:Tabuaeran_Kiribati.jpg', + 'http://localhost:8000', + 'https://datadryad.org/stash/downloads/file_stream/182482', + 'https://github.com/napari/docs/issues/new/choose', + 'https://github.com/napari/napari/issues/new/choose', + 'https://github.com/napari/napari/issues/new', + 'https://napari-hub.org', + 'https://github.com/napari/napari/releases/latest', + 'https://onlinelibrary.wiley.com/doi/10.1002/col.20327', ] @@ -461,21 +450,21 @@ def rewrite_github_anchor(app, uri: str): them before checking and makes them comparable. """ parsed = urlparse(uri) - if parsed.hostname == "github.com" and parsed.fragment: + if parsed.hostname == 'github.com' and parsed.fragment: for text in [ - "L", - "readme", - "pullrequestreview", - "issuecomment", - "issue", + 'L', + 'readme', + 'pullrequestreview', + 'issuecomment', + 'issue', ]: if parsed.fragment.startswith(text): return None - if re.match(r"r\d+", parsed.fragment): + if re.match(r'r\d+', parsed.fragment): return None - prefixed = parsed.fragment.startswith("user-content-") + prefixed = parsed.fragment.startswith('user-content-') if not prefixed: - fragment = f"user-content-{parsed.fragment}" + fragment = f'user-content-{parsed.fragment}' return urlunparse(parsed._replace(fragment=fragment)) return None @@ -489,10 +478,9 @@ def qt_docstrings(app, what, name, obj, options, lines): Avoids syntax errors since the Qt threading docstrings are written in Markdown, and injected into rst docstring automatically. """ - ignore_list = ["WorkerBase", "FunctionWorker", "GeneratorWorker"] - if any([f in name for f in ignore_list]): - if len(lines) > 0: - del lines[1:] + ignore_list = ['WorkerBase', 'FunctionWorker', 'GeneratorWorker'] + if any(f in name for f in ignore_list) and len(lines) > 0: + del lines[1:] # -- Docs build setup ------------------------------------------------------ @@ -509,12 +497,12 @@ def setup(app): * Cleans out Qt threading docstrings """ - app.registry.source_suffix.pop(".ipynb", None) - app.connect("source-read", add_google_calendar_secrets) - app.connect("linkcheck-process-uri", rewrite_github_anchor) - app.connect("autodoc-process-docstring", qt_docstrings) + app.registry.source_suffix.pop('.ipynb', None) + app.connect('source-read', add_google_calendar_secrets) + app.connect('linkcheck-process-uri', rewrite_github_anchor) + app.connect('autodoc-process-docstring', qt_docstrings) - logger = logging.getLogger("sphinx") + logger = logging.getLogger('sphinx') warning_handler, *_ = [ h for h in logger.handlers if isinstance(h, sphinx_logging.WarningStreamHandler) ] From 96185e5d2d5f96d4f7e66ec5570011672e6248eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Mon, 27 Oct 2025 14:43:45 +0900 Subject: [PATCH 2/5] Lint all markdown files --- .mdformat.toml | 3 + docs/_scripts/README.md | 36 +- docs/_scripts/_table_maker.py | 62 +- docs/_scripts/autogenerate_gui_images.py | 170 +++--- docs/_scripts/prep_docs.py | 49 +- docs/_scripts/update_event_docs.py | 42 +- docs/_scripts/update_preference_docs.py | 72 +-- docs/_scripts/update_release_docs.py | 142 +++-- docs/_scripts/update_ui_sections_docs.py | 571 +++++++++--------- docs/_static/custom.css | 2 +- docs/_templates/details_polygon_path_tool.md | 45 +- docs/_templates/sbt-sidebar-footer.html | 2 +- docs/api/index.md | 3 +- docs/community/code_of_conduct.md | 51 +- docs/community/code_of_conduct_reporting.md | 63 +- docs/community/governance.md | 17 +- docs/community/index.md | 12 +- docs/community/licensing.md | 4 +- docs/community/meeting_schedule.md | 2 + docs/community/mission_and_values.md | 12 +- docs/community/resources.md | 4 +- docs/community/team.md | 18 +- docs/community/working_groups.md | 11 +- .../architecture/dir_organization.md | 38 +- docs/developers/architecture/index.md | 4 +- .../architecture/magicgui_type_reg.md | 6 +- docs/developers/architecture/napari_models.md | 44 +- .../documentation/docs_deployment.md | 99 +-- .../documentation/docs_template.md | 30 +- docs/developers/contributing/index.md | 10 +- .../contributing/performance/benchmarks.md | 45 +- .../contributing/performance/index.md | 1 + .../contributing/performance/profiling.md | 91 +-- docs/developers/contributing/testing.md | 56 +- docs/developers/contributing/translations.md | 82 +-- docs/developers/coredev/core_dev_guide.md | 127 ++-- docs/developers/coredev/maintenance.md | 40 +- docs/developers/coredev/packaging.md | 46 +- docs/further-resources/napari-workshops.md | 102 ++-- docs/guides/3D_interactivity.md | 9 +- docs/guides/contexts_expressions.md | 166 ++--- docs/guides/event_loop.md | 12 +- docs/guides/events_reference.md | 6 +- docs/guides/handedness.md | 54 +- docs/guides/index.md | 1 + docs/guides/layers.md | 30 +- docs/guides/performance.md | 109 ++-- docs/guides/threading.md | 145 ++--- docs/guides/triangulation.md | 6 +- docs/howtos/docker.md | 40 +- docs/howtos/extending/connecting_events.md | 30 +- docs/howtos/extending/index.md | 5 +- docs/howtos/index.md | 5 +- docs/howtos/layers/image.md | 113 ++-- docs/howtos/layers/index.md | 3 +- docs/howtos/layers/labels.md | 178 +++--- docs/howtos/layers/points.md | 108 ++-- docs/howtos/layers/surface.md | 38 +- docs/howtos/layers/tracks.md | 113 ++-- docs/howtos/layers/vectors.md | 46 +- docs/howtos/napari_imageJ.md | 66 +- docs/howtos/perfmon.md | 15 +- docs/howtos/themes.md | 10 +- docs/naps/1-institutional-funding-partners.md | 1 - docs/naps/index.md | 1 + .../advanced_topics/adapted_plugin_guide.md | 9 +- docs/plugins/advanced_topics/npe1.md | 26 +- .../advanced_topics/npe2_migration_guide.md | 31 +- .../advanced_topics/widget_communication.md | 39 +- .../building_a_plugin/_layer_data_guide.md | 21 +- .../building_a_plugin/best_practices.md | 97 ++- .../building_a_plugin/debug_plugins.md | 60 +- docs/plugins/building_a_plugin/guides.md | 17 +- docs/plugins/building_a_plugin/index.md | 48 +- .../finding_and_installing_plugins.md | 8 +- docs/plugins/testing_and_publishing/deploy.md | 16 +- docs/plugins/testing_and_publishing/test.md | 14 +- .../1-pythons-assert-keyword.md | 35 +- .../2-pytest-testing-frameworks.md | 26 +- .../3-readers-and-fixtures.md | 45 +- .../testing_workshop_docs/4-test-coverage.md | 28 +- docs/plugins/testing_workshop_docs/index.md | 14 +- .../testing-resources.md | 17 +- .../1-virtual-environments.md | 23 +- .../2-deploying-your-plugin.md | 45 +- .../3-version-management.md | 36 +- .../4-developer-tools.md | 61 +- .../virtual_environment_docs/5-survey.md | 79 ++- .../plugins/virtual_environment_docs/index.md | 11 +- docs/release/release_0_1_0.md | 2 +- docs/release/release_0_2_0.md | 4 +- docs/release/release_0_2_11.md | 5 +- docs/release/release_0_2_12.md | 4 +- docs/release/release_0_2_4.md | 3 +- docs/release/release_0_2_5.md | 2 +- docs/release/release_0_3_0.md | 19 +- docs/release/release_0_3_1.md | 17 +- docs/release/release_0_3_2.md | 9 +- docs/release/release_0_3_3.md | 7 +- docs/release/release_0_3_4.md | 2 +- docs/release/release_0_3_5.md | 11 +- docs/release/release_0_3_6.md | 16 +- docs/release/release_0_3_7.md | 12 +- docs/release/release_0_3_8.md | 9 +- docs/release/release_0_4_0.md | 16 +- docs/release/release_0_4_1.md | 11 +- docs/release/release_0_4_10.md | 15 +- docs/release/release_0_4_11.md | 21 +- docs/release/release_0_4_12.md | 20 +- docs/release/release_0_4_13.md | 31 +- docs/release/release_0_4_14.md | 10 +- docs/release/release_0_4_15.md | 11 +- docs/release/release_0_4_16.md | 107 ++-- docs/release/release_0_4_17.md | 46 +- docs/release/release_0_4_18.md | 35 +- docs/release/release_0_4_19.md | 31 +- docs/release/release_0_4_2.md | 13 +- docs/release/release_0_4_3.md | 41 +- docs/release/release_0_4_4.md | 14 +- docs/release/release_0_4_5.md | 27 +- docs/release/release_0_4_6.md | 20 +- docs/release/release_0_4_7.md | 11 +- docs/release/release_0_4_8.md | 21 +- docs/release/release_0_4_9.md | 15 +- docs/release/release_0_5_0.md | 157 +++-- docs/release/release_0_5_1.md | 23 +- docs/release/release_0_5_2.md | 19 +- docs/release/release_0_5_3.md | 20 +- docs/release/release_0_5_4.md | 16 +- docs/release/release_0_5_5.md | 29 +- docs/release/release_0_5_6.md | 27 +- docs/release/release_0_6_0.md | 107 ++-- docs/release/release_0_6_1.md | 10 +- docs/release/release_0_6_2.md | 44 +- docs/release/release_0_6_3.md | 42 +- docs/release/release_0_6_4.md | 7 +- docs/release/release_0_6_5.md | 36 +- docs/release/release_0_6_6.md | 13 +- docs/roadmaps/0_3.md | 2 +- docs/roadmaps/0_3_retrospective.md | 38 +- docs/roadmaps/0_4.md | 3 +- docs/roadmaps/active_roadmap.md | 173 +++--- docs/roadmaps/index.md | 2 +- docs/troubleshooting.md | 46 +- docs/tutorials/annotation/annotate_points.md | 25 +- docs/tutorials/annotation/index.md | 3 +- docs/tutorials/fundamentals/features.md | 11 +- .../tutorials/fundamentals/getting_started.md | 21 +- docs/tutorials/fundamentals/quick_start.md | 9 +- docs/tutorials/processing/dask.md | 30 +- docs/tutorials/processing/index.md | 3 +- .../segmentation/annotate_segmentation.md | 65 +- docs/tutorials/segmentation/index.md | 3 +- docs/tutorials/tracking/cell_tracking.md | 36 +- docs/tutorials/tracking/index.md | 3 +- 155 files changed, 3126 insertions(+), 2865 deletions(-) create mode 100644 .mdformat.toml diff --git a/.mdformat.toml b/.mdformat.toml new file mode 100644 index 000000000..50a43501a --- /dev/null +++ b/.mdformat.toml @@ -0,0 +1,3 @@ +exclude = [ + "docs/naps/*.md", +] diff --git a/docs/_scripts/README.md b/docs/_scripts/README.md index c830fe44f..df26762d0 100644 --- a/docs/_scripts/README.md +++ b/docs/_scripts/README.md @@ -5,6 +5,7 @@ This directory contains Python scripts that automate various aspects of the napa ## Overview The `_scripts` folder is a critical part of the napari documentation infrastructure, containing automation tools that: + - Generate GUI screenshots automatically - Extract and document events from the codebase - Create preference documentation with dialog images @@ -18,12 +19,14 @@ The `_scripts` folder is a critical part of the napari documentation infrastruct **Purpose**: Master orchestration script that coordinates all documentation generation tasks. **Usage**: + ```bash python docs/_scripts/prep_docs.py python docs/_scripts/prep_docs.py --stubs # Fast mode with stub content ``` **Functionality**: + - Clones and processes the npe2 repository for plugin documentation - Executes all other documentation generation scripts in sequence - Supports a `--stubs` mode for faster development builds @@ -34,17 +37,20 @@ python docs/_scripts/prep_docs.py --stubs # Fast mode with stub content **Purpose**: Automatically captures screenshots of napari GUI components for use in documentation. **Usage**: + ```bash python docs/_scripts/autogenerate_gui_images.py ``` **Generated Content**: + - `docs/images/_autogenerated/widgets/` - Individual widget screenshots - `docs/images/_autogenerated/menus/` - Menu screenshots - `docs/images/_autogenerated/popups/` - Popup dialog screenshots - `docs/images/_autogenerated/regions/` - Combined component regions **Key Features**: + - Opens napari with sample data (cells3d) - Captures widgets, menus, popups, and viewer regions - Applies consistent dark theme @@ -55,17 +61,20 @@ python docs/_scripts/autogenerate_gui_images.py **Purpose**: Analyzes the napari codebase to generate comprehensive event documentation. **Usage**: + ```bash python docs/_scripts/update_event_docs.py python docs/_scripts/update_event_docs.py --stubs ``` **Generated Content**: + - `docs/guides/_viewer_events.md` - Viewer model events - `docs/guides/_layerlist_events.md` - Layer list events - `docs/guides/_layer_events.md` - Layer-specific events **Key Features**: + - Uses AST parsing to discover EmitterGroup definitions - Extracts documentation from docstrings - Creates formatted tables with event names, descriptions, access patterns, and types @@ -76,16 +85,19 @@ python docs/_scripts/update_event_docs.py --stubs **Purpose**: Generates preference documentation with screenshots of preference dialogs. **Usage**: + ```bash python docs/_scripts/update_preference_docs.py python docs/_scripts/update_preference_docs.py --stubs ``` **Generated Content**: + - `docs/guides/preferences.md` - Complete preferences documentation - `docs/images/_autogenerated/preferences/` - Preference dialog screenshots **Key Features**: + - Documents all settings from NapariSettings - Captures screenshots of each preference section - Uses Jinja2 templates for consistent formatting @@ -96,12 +108,14 @@ python docs/_scripts/update_preference_docs.py --stubs **Purpose**: Generates architecture documentation for napari's UI components with dependency analysis. **Usage**: + ```bash python docs/_scripts/update_ui_sections_docs.py python docs/_scripts/update_ui_sections_docs.py --stubs ``` **Generated Content**: + - `docs/developers/architecture/ui_sections/` - Architecture documentation for each UI section - `layers_list_ui.md` - `layers_controls_ui.md` @@ -112,16 +126,18 @@ python docs/_scripts/update_ui_sections_docs.py --stubs - `console_ui.md` **Key Features**: + - Uses pydeps for dependency analysis - Generates interactive Mermaid diagrams - Creates directory layout views - Links to source code on GitHub -### _table_maker.py +### `_table_maker.py` **Purpose**: Utility module for creating formatted ASCII/Markdown tables. **Usage**: + ```python from _table_maker import table_repr @@ -130,6 +146,7 @@ table = table_repr(data, style='markdown') ``` **Key Features**: + - Supports multiple border styles (double, heavy, light, markdown) - Auto-calculates column widths - Configurable padding and headers @@ -140,14 +157,17 @@ table = table_repr(data, style='markdown') These scripts integrate with the documentation build process through the Makefile: 1. **Full builds** (`make html`): + - Runs `prep_docs.py` which executes all scripts - Generates all images and documentation files -2. **Fast builds** (`make slimfast`): +1. **Fast builds** (`make slimfast`): + - May use `--stubs` mode for quicker iteration - Skips time-consuming generation tasks -3. **Gallery builds** (`make slimgallery`): +1. **Gallery builds** (`make slimgallery`): + - May trigger `autogenerate_gui_images.py` for GUI screenshots ## Development Notes @@ -157,9 +177,9 @@ These scripts integrate with the documentation build process through the Makefil When adding a new documentation generation script: 1. Follow the existing pattern of supporting `--stubs` mode for fast builds -2. Import and call your script from `prep_docs.py` -3. Generate content in appropriate documentation directories -4. Use relative imports for shared utilities like `_table_maker` +1. Import and call your script from `prep_docs.py` +1. Generate content in appropriate documentation directories +1. Use relative imports for shared utilities like `_table_maker` ### Debugging @@ -170,6 +190,7 @@ When adding a new documentation generation script: ### Dependencies Most scripts require: + - A working napari installation with Qt backend - Access to napari source code - Additional tools like pydeps for architecture documentation @@ -197,6 +218,7 @@ docs/ ## Maintenance These scripts should be updated when: + - New GUI components are added to napari - Event systems are modified - Preferences are added or changed @@ -205,7 +227,7 @@ These scripts should be updated when: Regular testing ensures the documentation stays synchronized with the codebase. ---- +______________________________________________________________________ ## Attribution diff --git a/docs/_scripts/_table_maker.py b/docs/_scripts/_table_maker.py index 8283dd8ea..4a33f9f04 100644 --- a/docs/_scripts/_table_maker.py +++ b/docs/_scripts/_table_maker.py @@ -52,29 +52,29 @@ import numpy as np STYLES = { - "double": { - "TOP": ("╔", "╤", "╗", "═"), - "MID": ("╟", "┼", "╢", "─"), - "BOT": ("╚", "╧", "╝", "═"), - "V": ("║", "│"), + 'double': { + 'TOP': ('╔', '╤', '╗', '═'), + 'MID': ('╟', '┼', '╢', '─'), + 'BOT': ('╚', '╧', '╝', '═'), + 'V': ('║', '│'), }, - "heavy": { - "TOP": ("┏", "┯", "┓", "━"), - "MID": ("┠", "┼", "┨", "─"), - "BOT": ("┗", "┷", "┛", "━"), - "V": ("┃", "│"), + 'heavy': { + 'TOP': ('┏', '┯', '┓', '━'), + 'MID': ('┠', '┼', '┨', '─'), + 'BOT': ('┗', '┷', '┛', '━'), + 'V': ('┃', '│'), }, - "light": { - "TOP": ("┌", "┬", "┐", "─"), - "MID": ("├", "┼", "┤", "─"), - "BOT": ("└", "┴", "┘", "─"), - "V": ("│", "│"), + 'light': { + 'TOP': ('┌', '┬', '┐', '─'), + 'MID': ('├', '┼', '┤', '─'), + 'BOT': ('└', '┴', '┘', '─'), + 'V': ('│', '│'), }, - "markdown": { - "TOP": (" ", " ", " ", " "), - "MID": ("|", "|", "|", "-"), - "BOT": (" ", " ", " ", " "), - "V": ("|", "|"), + 'markdown': { + 'TOP': (' ', ' ', ' ', ' '), + 'MID': ('|', '|', '|', '-'), + 'BOT': (' ', ' ', ' ', ' '), + 'V': ('|', '|'), }, } @@ -86,22 +86,22 @@ def table_repr( header=None, cell_width=None, divide_rows=True, - style="markdown", + style='markdown', ): """Pretty string repr of a 2D table.""" try: nrows = len(data) except TypeError: - raise TypeError("data must be a collection") + raise TypeError('data must be a collection') if not nrows: - return "" + return '' try: ncols = ncols or len(data[0]) except TypeError: - raise TypeError("data must be a collection") + raise TypeError('data must be a collection') except IndexError: - raise IndexError("data must be a 2D collection of collections") + raise IndexError('data must be a 2D collection of collections') _widths = list(data) if header: @@ -110,16 +110,14 @@ def table_repr( cell_widths = _widths.max(0).tolist() _style = STYLES[style] - TOP, MID, BOT, V = _style["TOP"], _style["MID"], _style["BOT"], _style["V"] + TOP, MID, BOT, V = _style['TOP'], _style['MID'], _style['BOT'], _style['V'] - pad = " " * padding - cell_templates = [ - (pad + "{{:{0}}}" + pad).format(max(cw, 5)) for cw in cell_widths - ] + pad = ' ' * padding + cell_templates = [(pad + '{{:{0}}}' + pad).format(max(cw, 5)) for cw in cell_widths] row_template = V[0] + V[1].join(cell_templates) + V[0] def _border(left, sep, right, line): - _cells = [len(ct.format("")) * line for ct in cell_templates] + _cells = [len(ct.format('')) * line for ct in cell_templates] return left + sep.join(_cells) + right body = [_border(*TOP)] @@ -134,4 +132,4 @@ def _border(left, sep, right, line): body.append(_border(*MID)) body.append(_border(*BOT)) - return "\n".join(body) + return '\n'.join(body) diff --git a/docs/_scripts/autogenerate_gui_images.py b/docs/_scripts/autogenerate_gui_images.py index d4c9a782f..9d9862da1 100644 --- a/docs/_scripts/autogenerate_gui_images.py +++ b/docs/_scripts/autogenerate_gui_images.py @@ -62,26 +62,26 @@ from pathlib import Path -from qtpy.QtCore import QTimer, QPoint, QRect import napari - +from napari._qt.dialogs.qt_modal import QtPopup from napari._qt.qt_event_loop import get_qapp from napari._qt.qt_resources import get_stylesheet -from napari._qt.dialogs.qt_modal import QtPopup +from qtpy.QtCore import QPoint, QRect, QTimer from qtpy.QtWidgets import QApplication, QWidget DOCS = REPO_ROOT_PATH = Path(__file__).resolve().parent.parent -IMAGES_PATH = DOCS / "images" / "_autogenerated" +IMAGES_PATH = DOCS / 'images' / '_autogenerated' IMAGES_PATH.mkdir(parents=True, exist_ok=True) -WIDGETS_PATH = IMAGES_PATH / "widgets" +WIDGETS_PATH = IMAGES_PATH / 'widgets' WIDGETS_PATH.mkdir(parents=True, exist_ok=True) -MENUS_PATH = IMAGES_PATH / "menus" +MENUS_PATH = IMAGES_PATH / 'menus' MENUS_PATH.mkdir(parents=True, exist_ok=True) -POPUPS_PATH = IMAGES_PATH / "popups" +POPUPS_PATH = IMAGES_PATH / 'popups' POPUPS_PATH.mkdir(parents=True, exist_ok=True) -REGION_PATH = IMAGES_PATH / "regions" +REGION_PATH = IMAGES_PATH / 'regions' REGION_PATH.mkdir(parents=True, exist_ok=True) + def _get_widget_components(qt_window: QWidget) -> dict: """Get visible widget components from the Qt window. @@ -97,25 +97,21 @@ def _get_widget_components(qt_window: QWidget) -> dict: and values corresponding to the QWidget itself """ return { - "welcome_widget": find_widget_by_class(qt_window, "QtWelcomeWidget"), - - "console_dock": find_widget_by_name(qt_window, "console"), - - "dimension_slider": find_widget_by_class(qt_window, "QtDims"), - + 'welcome_widget': find_widget_by_class(qt_window, 'QtWelcomeWidget'), + 'console_dock': find_widget_by_name(qt_window, 'console'), + 'dimension_slider': find_widget_by_class(qt_window, 'QtDims'), # Layer list components - "layer_list_dock": find_widget_by_name(qt_window, "layer list"), - "layer_buttons": find_widget_by_class(qt_window, "QtLayerButtons"), - "layer_list": find_widget_by_class(qt_window, "QtLayerList"), - "viewer_buttons": find_widget_by_class(qt_window, "QtViewerButtons"), - + 'layer_list_dock': find_widget_by_name(qt_window, 'layer list'), + 'layer_buttons': find_widget_by_class(qt_window, 'QtLayerButtons'), + 'layer_list': find_widget_by_class(qt_window, 'QtLayerList'), + 'viewer_buttons': find_widget_by_class(qt_window, 'QtViewerButtons'), # Layer controls - "layer_controls_dock": find_widget_by_name(qt_window, "layer controls"), - + 'layer_controls_dock': find_widget_by_name(qt_window, 'layer controls'), # TODO: mouse over part of the image to show intensity stuff - "status_bar": find_widget_by_class(qt_window, "ViewerStatusBar"), + 'status_bar': find_widget_by_class(qt_window, 'ViewerStatusBar'), } + def _get_menu_components(qt_window: QWidget) -> dict: """Get menu bar components from the Qt window. @@ -132,15 +128,16 @@ def _get_menu_components(qt_window: QWidget) -> dict: """ return { - "file_menu": find_widget_by_name(qt_window, "napari/file"), - "samples_menu": find_widget_by_name(qt_window, "napari/file/samples/napari"), - "view_menu": find_widget_by_name(qt_window, "napari/view"), - "layers_menu": find_widget_by_name(qt_window, "napari/layers"), - "plugins_menu": find_widget_by_name(qt_window, "napari/plugins"), - "window_menu": find_widget_by_name(qt_window, "napari/window"), - "help_menu": find_widget_by_name(qt_window, "napari/help"), + 'file_menu': find_widget_by_name(qt_window, 'napari/file'), + 'samples_menu': find_widget_by_name(qt_window, 'napari/file/samples/napari'), + 'view_menu': find_widget_by_name(qt_window, 'napari/view'), + 'layers_menu': find_widget_by_name(qt_window, 'napari/layers'), + 'plugins_menu': find_widget_by_name(qt_window, 'napari/plugins'), + 'window_menu': find_widget_by_name(qt_window, 'napari/window'), + 'help_menu': find_widget_by_name(qt_window, 'napari/help'), } + def _get_button_popups_configs( viewer: napari.Viewer, ) -> list[dict]: @@ -161,33 +158,31 @@ def _get_button_popups_configs( - button: QtViewerButton Button that opens the popup. """ - viewer_buttons = find_widget_by_class( - viewer.window._qt_window, - "QtViewerButtons" - ) + viewer_buttons = find_widget_by_class(viewer.window._qt_window, 'QtViewerButtons') return [ { - "name": "ndisplay_2D_popup", - "prep": lambda: setattr(viewer.dims, "ndisplay", 2), - "button": viewer_buttons.ndisplayButton, + 'name': 'ndisplay_2D_popup', + 'prep': lambda: setattr(viewer.dims, 'ndisplay', 2), + 'button': viewer_buttons.ndisplayButton, }, { - "name": "roll_dims_popup", - "prep": lambda: setattr(viewer.dims, "ndisplay", 2), - "button": viewer_buttons.rollDimsButton, + 'name': 'roll_dims_popup', + 'prep': lambda: setattr(viewer.dims, 'ndisplay', 2), + 'button': viewer_buttons.rollDimsButton, }, { - "name": "ndisplay_3D_popup", - "prep": lambda: setattr(viewer.dims, "ndisplay", 3), - "button": viewer_buttons.ndisplayButton, + 'name': 'ndisplay_3D_popup', + 'prep': lambda: setattr(viewer.dims, 'ndisplay', 3), + 'button': viewer_buttons.ndisplayButton, }, { - "name": "grid_popup", - "prep": None, - "button": viewer_buttons.gridViewButton, - } + 'name': 'grid_popup', + 'prep': None, + 'button': viewer_buttons.gridViewButton, + }, ] + def _get_viewer_regions() -> list[dict]: """Get regions of the viewer to capture as a single image. @@ -202,15 +197,16 @@ def _get_viewer_regions() -> list[dict]: """ return [ { - "name": "console_and_buttons", - "components": ["console_dock", "viewer_buttons"] + 'name': 'console_and_buttons', + 'components': ['console_dock', 'viewer_buttons'], }, { - "name": "layer_list_and_controls", - "components": ["layer_list_dock", "layer_controls_dock"] + 'name': 'layer_list_and_controls', + 'components': ['layer_list_dock', 'layer_controls_dock'], }, ] + def autogenerate_images(): """Autogenerate images of the GUI components. @@ -235,14 +231,14 @@ def autogenerate_images(): # print_widget_hierarchy(viewer.window._qt_window) viewer.window._qt_window.resize(1000, 800) - viewer.window._qt_window.setStyleSheet(get_stylesheet("dark")) + viewer.window._qt_window.setStyleSheet(get_stylesheet('dark')) # Ensure window is active viewer.window._qt_window.activateWindow() viewer.window._qt_window.raise_() app.processEvents() - viewer.screenshot(str(IMAGES_PATH / "viewer_empty.png"), canvas_only=False) + viewer.screenshot(str(IMAGES_PATH / 'viewer_empty.png'), canvas_only=False) viewer.open_sample(plugin='napari', sample='cells3d') # Mouse over canvas for status bar update @@ -250,16 +246,18 @@ def autogenerate_images(): viewer.mouse_over_canvas = True viewer.cursor.position = [25, 50, 120] viewer.update_status_from_cursor() - app.processEvents() # Ensure viewer is fully initialized + app.processEvents() # Ensure viewer is fully initialized - viewer.screenshot(str(IMAGES_PATH / "viewer_cells3d.png"), canvas_only=False) + viewer.screenshot(str(IMAGES_PATH / 'viewer_cells3d.png'), canvas_only=False) # Open the console - viewer_buttons = find_widget_by_class(viewer.window._qt_window, "QtViewerButtons") + viewer_buttons = find_widget_by_class(viewer.window._qt_window, 'QtViewerButtons') viewer_buttons.consoleButton.click() app.processEvents() - viewer.screenshot(str(IMAGES_PATH / "viewer_cells3d_console.png"), canvas_only=False) + viewer.screenshot( + str(IMAGES_PATH / 'viewer_cells3d_console.png'), canvas_only=False + ) widget_componenets = _get_widget_components(viewer.window._qt_window) for name, widget in widget_componenets.items(): @@ -274,56 +272,64 @@ def autogenerate_images(): capture_popups(config) for region in _get_viewer_regions(): - capture_viewer_region(viewer, region["components"], region["name"]) + capture_viewer_region(viewer, region['components'], region['name']) close_all(viewer) app.exec_() + def capture_popups(config): """Capture popups that appear when clicking on viewer buttons.""" app = get_qapp() close_existing_popups() - if config["prep"] is not None: - config["prep"]() + if config['prep'] is not None: + config['prep']() app.processEvents() - config["button"].customContextMenuRequested.emit(QPoint()) + config['button'].customContextMenuRequested.emit(QPoint()) app.processEvents() - popups = [w for w in QApplication.topLevelWidgets() if isinstance(w, QtPopup) and w.isVisible()] + popups = [ + w + for w in QApplication.topLevelWidgets() + if isinstance(w, QtPopup) and w.isVisible() + ] if not popups: - return print(f"No popup found for {config['name']}") + return print(f'No popup found for {config["name"]}') - popup = popups[-1] # grab the most recent popup, just in case + popup = popups[-1] # grab the most recent popup, just in case app.processEvents() pixmap = popup.grab() - pixmap.save(str(POPUPS_PATH / f"{config['name']}.png")) + pixmap.save(str(POPUPS_PATH / f'{config["name"]}.png')) popup.close() app.processEvents() + def capture_widget(widget, name): """Capture a widget and save it to a file.""" if widget is None: - return print(f"Could not find {name}") + return print(f'Could not find {name}') pixmap = widget.grab() - pixmap.save(str(WIDGETS_PATH / f"{name}.png")) - return + pixmap.save(str(WIDGETS_PATH / f'{name}.png')) + return None + def capture_menu(menu, name): """Show a menu and take screenshot of it.""" if menu is None: - return print(f"Could not find menu {name}") + return print(f'Could not find menu {name}') menu.popup(menu.parent().mapToGlobal(menu.pos())) pixmap = menu.grab() - pixmap.save(str(MENUS_PATH / f"{name}.png")) + pixmap.save(str(MENUS_PATH / f'{name}.png')) menu.hide() - return + return None + def capture_viewer_region(viewer, component_names, save_name): """Capture a screenshot of a region containing multiple components. @@ -349,7 +355,7 @@ def capture_viewer_region(viewer, component_names, save_name): for name in component_names: if name not in widget_components or widget_components[name] is None: - print(f"Component {name} not found, skipping") + print(f'Component {name} not found, skipping') continue widget = widget_components[name] @@ -364,20 +370,24 @@ def capture_viewer_region(viewer, component_names, save_name): max_y = max(max_y, global_rect.bottom()) if min_x == float('inf'): - print(f"No valid components found for {save_name}") + print(f'No valid components found for {save_name}') return region = QRect(QPoint(min_x, min_y), QPoint(max_x, max_y)) app.processEvents() screen = QApplication.primaryScreen() - pixmap = screen.grabWindow(0, region.x(), region.y(), region.width(), region.height()) - pixmap.save(str(REGION_PATH / f"{save_name}.png")) + pixmap = screen.grabWindow( + 0, region.x(), region.y(), region.width(), region.height() + ) + pixmap.save(str(REGION_PATH / f'{save_name}.png')) + def close_all(viewer): viewer.close() QTimer.singleShot(10, lambda: get_qapp().quit()) + def close_existing_popups(): """Close any existing popups.""" for widget in QApplication.topLevelWidgets(): @@ -386,6 +396,7 @@ def close_existing_popups(): get_qapp().processEvents() + def find_widget_by_name(parent, name): """Find a widget by its object name.""" if parent.objectName() == name: @@ -402,6 +413,7 @@ def find_widget_by_name(parent, name): return None + def find_widget_by_class(parent, class_name): """Find a child widget by its class name.""" if parent.__class__.__name__ == class_name: @@ -427,13 +439,13 @@ def print_widget_hierarchy(widget, indent=0, max_depth=None): class_name = widget.__class__.__name__ object_name = widget.objectName() - name_str = f" (name: '{object_name}')" if object_name else "" - print(" " * indent + f"- {class_name}{name_str}") + name_str = f" (name: '{object_name}')" if object_name else '' + print(' ' * indent + f'- {class_name}{name_str}') for child in widget.children(): - if hasattr(child, "children"): + if hasattr(child, 'children'): print_widget_hierarchy(child, indent + 4, max_depth) -if __name__ == "__main__": - autogenerate_images() \ No newline at end of file +if __name__ == '__main__': + autogenerate_images() diff --git a/docs/_scripts/prep_docs.py b/docs/_scripts/prep_docs.py index 2f62a0ef0..3932971ec 100644 --- a/docs/_scripts/prep_docs.py +++ b/docs/_scripts/prep_docs.py @@ -39,50 +39,55 @@ This docstring was drafted with the assistance of Claude Code. The output was reviewed and edited for accuracy and clarity. """ + import sys -from pathlib import Path from importlib.metadata import version +from pathlib import Path from packaging.version import parse DOCS = Path(__file__).parent.parent.absolute() NPE = DOCS.parent.absolute() / 'npe2' + def prep_npe2(): # some plugin docs live in npe2 for testing purposes if NPE.exists(): return from subprocess import check_call - npe2_version = version("npe2") + npe2_version = version('npe2') - check_call(f"rm -rf {NPE}".split()) - check_call(f"git clone https://github.com/napari/npe2 {NPE}".split()) + check_call(f'rm -rf {NPE}'.split()) + check_call(f'git clone https://github.com/napari/npe2 {NPE}'.split()) if not parse(npe2_version).is_devrelease: - check_call(f"git -c advice.detachedHead=false checkout tags/v{npe2_version}".split(), cwd=NPE) - check_call([sys.executable, f"{NPE}/_docs/render.py", DOCS / 'plugins']) - check_call(f"rm -rf {NPE}".split()) + check_call( + f'git -c advice.detachedHead=false checkout tags/v{npe2_version}'.split(), + cwd=NPE, + ) + check_call([sys.executable, f'{NPE}/_docs/render.py', DOCS / 'plugins']) + check_call(f'rm -rf {NPE}'.split()) def main(stubs=False): if stubs: - #prep_npe2() + # prep_npe2() # Generate stub files for plugin docs plugin_docs = { - "plugins/_npe2_sample_data_guide.md": "(sample-data-contribution-guide)=\n", - "plugins/_npe2_readers_guide.md": "(readers-contribution-guide)=\n", - "plugins/_npe2_widgets_guide.md": "(widgets-contribution-guide)=\n", - "plugins/_npe2_menus_guide.md": "(menus-contribution-guide)=\n", - "plugins/_npe2_manifest.md": "# Manifest Reference\n", - "plugins/_npe2_writers_guide.md": "(writers-contribution-guide)=\n", - "plugins/_npe2_contributions.md": "# Contributions Reference\n(contributions-themes)=\n(contributions-commands)=\n(contributions-widgets)=\n(contributions-readers)=\n(contributions-writers)=\n(contributions-sample-data)=\n(layer-type-constraints)=\n", + 'plugins/_npe2_sample_data_guide.md': '(sample-data-contribution-guide)=\n', + 'plugins/_npe2_readers_guide.md': '(readers-contribution-guide)=\n', + 'plugins/_npe2_widgets_guide.md': '(widgets-contribution-guide)=\n', + 'plugins/_npe2_menus_guide.md': '(menus-contribution-guide)=\n', + 'plugins/_npe2_manifest.md': '# Manifest Reference\n', + 'plugins/_npe2_writers_guide.md': '(writers-contribution-guide)=\n', + 'plugins/_npe2_contributions.md': '# Contributions Reference\n(contributions-themes)=\n(contributions-commands)=\n(contributions-widgets)=\n(contributions-readers)=\n(contributions-writers)=\n(contributions-sample-data)=\n(layer-type-constraints)=\n', } for doc, target in plugin_docs.items(): file_path = DOCS / doc if not file_path.exists(): # Avoid overwriting existing files file_path.write_text( - f"{target}This is a stub. The real file is autogenerated in a full build.", - encoding="utf-8", + f'{target}This is a stub. The real file is autogenerated in a full build.', + encoding='utf-8', ) # Generate stub files from the other scripts __import__('update_preference_docs').main(stubs=True) @@ -97,14 +102,14 @@ def main(stubs=False): __import__('update_release_docs').main() -if __name__ == "__main__": +if __name__ == '__main__': import argparse - parser = argparse.ArgumentParser(description="Prepare documentation files.") + parser = argparse.ArgumentParser(description='Prepare documentation files.') parser.add_argument( - "--stubs", - action="store_true", - help="Generate stubs versions of the documentation files.", + '--stubs', + action='store_true', + help='Generate stubs versions of the documentation files.', ) args = parser.parse_args() diff --git a/docs/_scripts/update_event_docs.py b/docs/_scripts/update_event_docs.py index 2af9a418f..e12f9fe39 100644 --- a/docs/_scripts/update_event_docs.py +++ b/docs/_scripts/update_event_docs.py @@ -49,20 +49,20 @@ import ast import inspect +from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path from types import ModuleType -from typing import Dict, Iterator, List, Optional, Type +from typing import Dict, List, Optional, Type +import napari import numpy as np from _table_maker import table_repr -from numpydoc.docscrape import ClassDoc, Parameter - -import napari from napari import layers from napari.components.layerlist import LayerList from napari.components.viewer_model import ViewerModel from napari.utils.events import EventedModel +from numpydoc.docscrape import ClassDoc, Parameter DOCS = Path(__file__).parent.parent @@ -95,7 +95,7 @@ def type_name(self): if cls_name := getattr(self.type_, '__name__', None): return cls_name name = str(self.type_) if self.type_ else '' - return name.replace("typing.", "") + return name.replace('typing.', '') def ev_model_row(self) -> List[str]: return [ @@ -143,10 +143,8 @@ 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')} - ) + 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 @@ -159,11 +157,7 @@ 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_) @@ -290,8 +284,8 @@ def main(stubs=False): for file_path in stub_files: if not file_path.exists(): # Avoid overwriting existing files file_path.write_text( - "This is a stub. The real file is autogenerated in a full build.", - encoding="utf-8", + 'This is a stub. The real file is autogenerated in a full build.', + encoding='utf-8', ) else: HEADER = [ @@ -312,9 +306,7 @@ def main(stubs=False): # 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) @@ -327,9 +319,7 @@ def main(stubs=False): 'Description', 'Event.value type', ] - rows = [ - [ev.layer_row()[0]] + ev.layer_row()[2:] for ev in iter_layer_events() - ] + 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) @@ -338,11 +328,11 @@ def main(stubs=False): if __name__ == '__main__': import argparse - parser = argparse.ArgumentParser(description="Update event docs.") + parser = argparse.ArgumentParser(description='Update event docs.') parser.add_argument( - "--stubs", - action="store_true", - help="Generate stubs versions of the event docs.", + '--stubs', + action='store_true', + help='Generate stubs versions of the event docs.', ) args = parser.parse_args() diff --git a/docs/_scripts/update_preference_docs.py b/docs/_scripts/update_preference_docs.py index 89ae6cd1c..744c8e71e 100644 --- a/docs/_scripts/update_preference_docs.py +++ b/docs/_scripts/update_preference_docs.py @@ -60,17 +60,16 @@ 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.settings import NapariSettings +from qtpy.QtCore import QTimer +from qtpy.QtWidgets import QMessageBox DOCS = REPO_ROOT_PATH = Path(__file__).resolve().parent.parent -GUIDES_PATH = DOCS / "guides" -IMAGES_PATH = DOCS / "images" / "_autogenerated" +GUIDES_PATH = DOCS / 'guides' +IMAGES_PATH = DOCS / 'images' / '_autogenerated' IMAGES_PATH.mkdir(parents=True, exist_ok=True) PREFERENCES_TEMPLATE = """(napari-preferences)= @@ -228,14 +227,16 @@ def generate_images(): app = get_qapp() pref = PreferencesDialog() - pref.setStyleSheet(get_stylesheet("dark")) + pref.setStyleSheet(get_stylesheet('dark')) pref.show() QTimer.singleShot(1000, pref.close) # Collect all sections first - sections = [field.field_info.title or name - for name, field in NapariSettings.__fields__.items() - if isinstance(field.type_, ModelMetaclass)] + sections = [ + field.field_info.title or name + for name, field in NapariSettings.__fields__.items() + if isinstance(field.type_, ModelMetaclass) + ] # Process each section with proper timing for idx, title in enumerate(sections): @@ -248,14 +249,12 @@ def generate_images(): # Capture screenshot pixmap = pref.grab() - pixmap.save(str(IMAGES_PATH / f"preferences-{title.lower()}.png")) - - + pixmap.save(str(IMAGES_PATH / f'preferences-{title.lower()}.png')) box = QMessageBox( QMessageBox.Icon.Question, - "Restore Settings", - "Are you sure you want to restore default settings?", + 'Restore Settings', + 'Are you sure you want to restore default settings?', QMessageBox.RestoreDefaults | QMessageBox.Cancel, pref, ) @@ -263,7 +262,7 @@ def generate_images(): def grab(): pixmap = box.grab() - pixmap.save(str(IMAGES_PATH / "preferences-reset.png")) + pixmap.save(str(IMAGES_PATH / 'preferences-reset.png')) box.reject() QTimer.singleShot(300, grab) @@ -275,23 +274,24 @@ def create_preferences_docs(): sections = {} for name, field in NapariSettings.__fields__.items(): - if not isinstance(field.type_, ModelMetaclass): continue - excluded = getattr(field.type_.NapariConfig, "preferences_exclude", []) + excluded = getattr(field.type_.NapariConfig, 'preferences_exclude', []) title = field.field_info.title or name sections[title.lower()] = { - "title": title, - "description": field.field_info.description or '', - "fields": [ + 'title': title, + 'description': field.field_info.description or '', + 'fields': [ { - "field": n, - "title": f.field_info.title, - "description": f.field_info.description, - "default": f.get_default() if title.lower() == "shortcuts" else repr(f.get_default()), - "ui": n not in excluded, - "type": repr(f._type_display()).replace('.typing', ''), + 'field': n, + 'title': f.field_info.title, + 'description': f.field_info.description, + 'default': f.get_default() + if title.lower() == 'shortcuts' + else repr(f.get_default()), + 'ui': n not in excluded, + 'type': repr(f._type_display()).replace('.typing', ''), } for n, f in sorted(field.type_.__fields__.items()) if n not in ('schema_version') @@ -306,33 +306,33 @@ def create_preferences_docs(): # in napari/napari.github.io, they are located at # guides/stable/images/_autogenerated text = Template(PREFERENCES_TEMPLATE).render( - sections=sections, images_path="../images/_autogenerated" + sections=sections, images_path='../images/_autogenerated' ) - (GUIDES_PATH / "preferences.md").write_text(text) + (GUIDES_PATH / 'preferences.md').write_text(text) def main(stubs=False): if stubs: # Generate stubs file - file_path = GUIDES_PATH / "preferences.md" + file_path = GUIDES_PATH / 'preferences.md' if not file_path.exists(): # Avoid overwriting existing files file_path.write_text( - "(napari-preferences)=\n# Preferences\nThis is a stub. The real file is autogenerated in a full build.", - encoding="utf-8", + '(napari-preferences)=\n# Preferences\nThis is a stub. The real file is autogenerated in a full build.', + encoding='utf-8', ) else: generate_images() create_preferences_docs() -if __name__ == "__main__": +if __name__ == '__main__': import argparse - parser = argparse.ArgumentParser(description="Update preference docs.") + parser = argparse.ArgumentParser(description='Update preference docs.') parser.add_argument( - "--stubs", - action="store_true", - help="Generate stubs versions of the preference docs.", + '--stubs', + action='store_true', + help='Generate stubs versions of the preference docs.', ) args = parser.parse_args() diff --git a/docs/_scripts/update_release_docs.py b/docs/_scripts/update_release_docs.py index 6eee1b6c7..cbd1624ed 100644 --- a/docs/_scripts/update_release_docs.py +++ b/docs/_scripts/update_release_docs.py @@ -65,7 +65,7 @@ # Path constants DOCS = Path(__file__).parent.parent.absolute() -RELEASE_PATH = DOCS / "release" +RELEASE_PATH = DOCS / 'release' # MyST template for the release notes index page RELEASE_INDEX_TEMPLATE = """(release-notes)= @@ -104,7 +104,7 @@ def extract_date_from_release(content: str) -> Optional[datetime]: """Extract release date from markdown content.""" lines = content.split('\n')[:10] - + for line in lines: # Pattern for dates like "*Thursday, Jul 11, 2024*" date_match = re.search(r'\*([^*]+)\*', line) @@ -113,10 +113,10 @@ def extract_date_from_release(content: str) -> Optional[datetime]: try: # Try different date formats for fmt in [ - "%A, %b %d, %Y", # Thursday, Jul 11, 2024 - "%a, %b %d, %Y", # Thu, Jul 11, 2024 - "%B %d, %Y", # July 11, 2024 - "%b %d, %Y", # Jul 11, 2024 + '%A, %b %d, %Y', # Thursday, Jul 11, 2024 + '%a, %b %d, %Y', # Thu, Jul 11, 2024 + '%B %d, %Y', # July 11, 2024 + '%b %d, %Y', # Jul 11, 2024 ]: try: return datetime.strptime(date_str, fmt) @@ -124,7 +124,7 @@ def extract_date_from_release(content: str) -> Optional[datetime]: continue except: pass - + return None @@ -133,18 +133,18 @@ def extract_highlights(content: str) -> str: lines = content.split('\n') highlights_lines = [] in_highlights = False - + for line in lines: if line.strip().lower().startswith('## highlights'): in_highlights = True continue - elif in_highlights and line.startswith('## '): + if in_highlights and line.startswith('## '): # End of highlights section break - elif in_highlights: + if in_highlights: # Keep the original formatting including headers highlights_lines.append(line) - + # Join back into a single string, preserving original formatting return '\n'.join(highlights_lines).strip() @@ -158,50 +158,50 @@ def extract_version_from_filename(filename: str) -> str: def generate_release_dropdowns(releases: List[Dict]) -> str: - """Generate dropdown sections for releases.""" - content = "" - + """Generate dropdown sections for releases.""" + content = '' + for release in releases[:6]: # Max 6 releases to avoid clutter - date_str = release['date'].strftime("%B %Y") - + date_str = release['date'].strftime('%B %Y') + # Use dropdown/details instead of tabs - content += f"````{{dropdown}} {release['title']} ({date_str})\n" - content += ":open:\n\n" - + content += f'````{{dropdown}} {release["title"]} ({date_str})\n' + content += ':open:\n\n' + # Add the full highlights content if release['highlights']: highlights = release['highlights'].strip() - content += f"{highlights}\n\n" - - content += f"[View full release notes →]({release['filename']})\n\n" - content += "````\n\n" - + content += f'{highlights}\n\n' + + content += f'[View full release notes →]({release["filename"]})\n\n' + content += '````\n\n' + return content def generate_release_list(releases: List[Dict]) -> str: """Generate simple list for older releases.""" - content = "" - + content = '' + for release in releases: if release['date']: - date_str = release['date'].strftime("%B %Y") - content += f"- **[{release['title']}]({release['filename']})** ({date_str})" + date_str = release['date'].strftime('%B %Y') + content += f'- **[{release["title"]}]({release["filename"]})** ({date_str})' else: # For releases without dates, just show the title - content += f"- **[{release['title']}]({release['filename']})**" - + content += f'- **[{release["title"]}]({release["filename"]})**' + if release['highlights']: # Show first few lines as preview first_lines = release['highlights'].split('\n')[:2] preview = ' '.join(first_lines).strip() if len(preview) > 150: - preview = preview[:150] + "..." + preview = preview[:150] + '...' if preview: - content += f" - {preview}" - - content += "\n" - + content += f' - {preview}' + + content += '\n' + return content @@ -209,20 +209,20 @@ def parse_releases() -> List[Dict]: """Parse all release files and extract information.""" releases_with_dates = [] releases_without_dates = [] - + # Parse all release files for release_file in RELEASE_PATH.glob('release_*.md'): - with open(release_file, 'r', encoding='utf-8') as f: + with open(release_file, encoding='utf-8') as f: content = f.read() - + version = extract_version_from_filename(release_file.name) release_date = extract_date_from_release(content) highlights = extract_highlights(content) - + # Extract title title_match = re.search(r'^# (.+)$', content, re.MULTILINE) - title = title_match.group(1) if title_match else f"napari {version}" - + title = title_match.group(1) if title_match else f'napari {version}' + if highlights: # Include releases with highlights (even without dates) release_info = { 'version': version, @@ -231,7 +231,7 @@ def parse_releases() -> List[Dict]: 'highlights': highlights, # Full highlights section as-is 'filename': release_file.stem, } - + if release_date: releases_with_dates.append(release_info) else: @@ -239,7 +239,7 @@ def parse_releases() -> List[Dict]: # Sort releases with dates by date (newest first) releases_with_dates.sort(key=lambda x: x['date'], reverse=True) - + # Sort releases without dates by version (attempt semantic sorting, newest first) def version_key(release): try: @@ -259,47 +259,55 @@ def version_key(release): def create_release_index_docs(): """Create release index docs from release files using a jinja template.""" - + # Parse all releases releases = parse_releases() - + # Group releases by time periods now = datetime.now() three_months_ago = now - timedelta(days=90) six_months_ago = now - timedelta(days=180) one_year_ago = now - timedelta(days=365) - + # Separate releases with and without dates dated_releases = [r for r in releases if r['date'] is not None] undated_releases = [r for r in releases if r['date'] is None] - + # Group dated releases by time periods recent = [r for r in dated_releases if r['date'] >= three_months_ago] - earlier_this_year = [r for r in dated_releases if six_months_ago <= r['date'] < three_months_ago] - this_year = [r for r in dated_releases if one_year_ago <= r['date'] < six_months_ago] + earlier_this_year = [ + r for r in dated_releases if six_months_ago <= r['date'] < three_months_ago + ] + this_year = [ + r for r in dated_releases if one_year_ago <= r['date'] < six_months_ago + ] older_dated = [r for r in dated_releases if r['date'] < one_year_ago] - + # Combine older dated releases with undated releases for the "Older Releases" section older = older_dated + undated_releases - + # Generate content for each section template_vars = { - 'last_updated': now.strftime("%B %d, %Y"), + 'last_updated': now.strftime('%B %d, %Y'), 'recent': recent, - 'recent_content': generate_release_dropdowns(recent) if recent else "", + 'recent_content': generate_release_dropdowns(recent) if recent else '', 'earlier_this_year': earlier_this_year, - 'earlier_content': generate_release_dropdowns(earlier_this_year) if earlier_this_year else "", + 'earlier_content': generate_release_dropdowns(earlier_this_year) + if earlier_this_year + else '', 'this_year': this_year, - 'this_year_content': generate_release_dropdowns(this_year) if this_year else "", + 'this_year_content': generate_release_dropdowns(this_year) if this_year else '', 'older': older, - 'older_content': generate_release_list(older) if older else "", # Show all older releases + 'older_content': generate_release_list(older) + if older + else '', # Show all older releases } - + # Generate the page content text = Template(RELEASE_INDEX_TEMPLATE).render(**template_vars) - + # Write the file - output_file = RELEASE_PATH / "index.md" + output_file = RELEASE_PATH / 'index.md' output_file.write_text(text, encoding='utf-8') @@ -307,24 +315,24 @@ def main(stubs=False): """Main entry point for generating release notes documentation.""" if stubs: # Generate stub file - file_path = RELEASE_PATH / "index.md" + file_path = RELEASE_PATH / 'index.md' if not file_path.exists(): # Avoid overwriting existing files file_path.write_text( - "(release-notes)=\n# Release Notes\nThis is a stub. The real file is autogenerated in a full build.", - encoding="utf-8", + '(release-notes)=\n# Release Notes\nThis is a stub. The real file is autogenerated in a full build.', + encoding='utf-8', ) else: create_release_index_docs() -if __name__ == "__main__": +if __name__ == '__main__': import argparse - parser = argparse.ArgumentParser(description="Update release notes index.") + parser = argparse.ArgumentParser(description='Update release notes index.') parser.add_argument( - "--stubs", - action="store_true", - help="Generate stub version of the release notes index.", + '--stubs', + action='store_true', + help='Generate stub version of the release notes index.', ) args = parser.parse_args() diff --git a/docs/_scripts/update_ui_sections_docs.py b/docs/_scripts/update_ui_sections_docs.py index b918b7fa1..33e3a39c4 100644 --- a/docs/_scripts/update_ui_sections_docs.py +++ b/docs/_scripts/update_ui_sections_docs.py @@ -73,25 +73,25 @@ import json from pathlib import Path +import seedir as sd +from napari._qt import dialogs, qt_viewer +from napari._qt._qapp_model import qactions + # ---- Napari imports 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_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 # ---- General constants # Docs paths DOCS = Path(__file__).parent.parent -UI_SECTIONS_DOCS_ROOT_PATH = DOCS / "developers" / "architecture" / "ui_sections" +UI_SECTIONS_DOCS_ROOT_PATH = DOCS / 'developers' / 'architecture' / 'ui_sections' # Napari and Napari UI sections modules paths NAPARI_ROOT_DIRECTORY_PATH = Path(qt_layer_list.__file__).parent.parent.parent @@ -128,16 +128,16 @@ def generate_dependencies_graph(options): It can return `None` depending on the options passed. """ - colors.START_COLOR = options["start_color"] - target = Target(options["fname"]) + colors.START_COLOR = options['start_color'] + target = Target(options['fname']) with target.chdir_work(): dep_graph = py2depgraph.py2dep(target, **options) dot_src = depgraph_to_dotsrc(target, dep_graph, **options) graph_content = None - if not options["no_output"]: - graph_content = dot.call_graphviz_dot(dot_src, options["format"]) - if options["output"]: - output_file = Path(options["output"]) + if not options['no_output']: + graph_content = dot.call_graphviz_dot(dot_src, options['format']) + if options['output']: + output_file = Path(options['output']) output_file.parent.mkdir(exist_ok=True, parents=True) output_file.write_bytes(graph_content) return dep_graph, dot_src, graph_content @@ -172,43 +172,43 @@ def generate_directory_layout( dependencies_dict = json.loads(str(dependencies_graph)) files_to_include = [] for dependency in dependencies_dict.values(): - if dependency["path"]: - files_to_include.append(Path(dependency["path"])) + if dependency['path']: + files_to_include.append(Path(dependency['path'])) def directory_layout_mask(item): item_path = Path(item) if item in files_to_include: return True - elif item_path.is_dir(): + if item_path.is_dir(): for file_to_include in files_to_include: if Path(file_to_include).is_relative_to(item_path): return True else: return False - directory_layout_content = "```\n" + directory_layout_content = '```\n' directory_layout_content += ( sd.seedir( path=root_directory, - style="lines", + style='lines', printout=False, mask=directory_layout_mask, ) - + "\n" + + '\n' ) - directory_layout_content += "```\n" + directory_layout_content += '```\n' if output_file: output_file = Path(output_file) output_file.parent.mkdir(exist_ok=True, parents=True) - output_file.write_text(directory_layout_content, encoding="utf-8") + output_file.write_text(directory_layout_content, encoding='utf-8') return directory_layout_content def generate_mermaid_diagram( dependencies_graph, - graph_orientation="LR", + graph_orientation='LR', graph_node_default_style=None, graph_node_external_style=None, graph_link_default_style=None, @@ -263,40 +263,33 @@ def generate_mermaid_diagram( """ dependencies_dict = json.loads(str(dependencies_graph)) - mermaid_diagram_content = "```{mermaid}\n" - mermaid_diagram_content += f"graph {graph_orientation or 'LR'}\n" + mermaid_diagram_content = '```{mermaid}\n' + mermaid_diagram_content += f'graph {graph_orientation or "LR"}\n' if graph_title: - mermaid_diagram_content += f"\taccTitle: {graph_title}\n" + mermaid_diagram_content += f'\taccTitle: {graph_title}\n' if graph_description: - mermaid_diagram_content += f"\taccDescr: {graph_description}\n" + mermaid_diagram_content += f'\taccDescr: {graph_description}\n' external_nodes = [] subgraphs = {} for dependency in dependencies_dict.values(): - dep_name = dependency["name"] - mermaid_diagram_content += f"\t{dep_name}({dep_name})\n" - if "imports" in dependency: - dep_imports = dependency["imports"] + dep_name = dependency['name'] + mermaid_diagram_content += f'\t{dep_name}({dep_name})\n' + 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 graph_urls_prefix and dependency["path"]: - module_path = Path(dependency["path"]) + 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 # a plugin such as `napari_console` if module_path.is_relative_to(NAPARI_ROOT_DIRECTORY_PATH): module_relative_path = module_path.relative_to( 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' - ) - dep_name_parent = ".".join(dep_name.split(".")[:-1]) + module_url = f'{graph_urls_prefix}{module_relative_path}' + 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] else: @@ -305,43 +298,33 @@ 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" + 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" - mermaid_diagram_content += "\tclass module.external subgraphs\n" + mermaid_diagram_content += f'\t\t {external_node}\n' + mermaid_diagram_content += '\tend\n' + mermaid_diagram_content += '\tclass module.external subgraphs\n' if subgraphs: mermaid_diagram_content += ( - "\tclassDef subgraphs fill:white,strock:black,color:black;" + '\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" + mermaid_diagram_content += f'\tclass {external_dep} external\n' - mermaid_diagram_content += "```\n" + mermaid_diagram_content += '```\n' return mermaid_diagram_content @@ -378,14 +361,16 @@ def generate_docs_ui_section_page( The UI section page content. """ - page_content = f"## {section_name}\n" - page_content += "### Dependencies diagram (related `napari` modules)\n" + 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) - output_file.write_text(page_content, encoding="utf-8") + output_file.write_text(page_content, encoding='utf-8') return page_content @@ -435,11 +420,11 @@ def generate_docs_ui_section( pydeps_graph, ) = generate_dependencies_graph(options) graph_title = ( - f"Dependencies between modules in the napari {section_name} UI section" + f'Dependencies between modules in the napari {section_name} UI section' ) graph_description = ( - "Diagram showing the dependencies between the modules " - f"involved in the definition of the napari {section_name} UI section" + 'Diagram showing the dependencies between the modules ' + f'involved in the definition of the napari {section_name} UI section' ) mermaid_graph = generate_mermaid_diagram( dep_graph, @@ -461,48 +446,48 @@ def generate_docs_ui_section( def main(stubs=False): # General 'settings' mermaid_graph_base_settings = { - "graph_orientation": "LR", - "graph_node_default_style": "fill:#00c3ff,color:black;", - "graph_node_external_style": "fill:#ffa600,color:black;", - "graph_link_default_style": "stroke:#00c3ff", - "graph_urls_prefix": "https://github.com/napari/napari/tree/main/napari/", + 'graph_orientation': 'LR', + 'graph_node_default_style': 'fill:#00c3ff,color:black;', + 'graph_node_external_style': 'fill:#ffa600,color:black;', + '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_name = "Layers list" - layer_list_output_page = UI_SECTIONS_DOCS_ROOT_PATH / "layers_list_ui.md" + layer_list_section_name = 'Layers list' + layer_list_output_page = UI_SECTIONS_DOCS_ROOT_PATH / 'layers_list_ui.md' layer_list_pydeps_args = [ - f"{LAYER_LIST_MODULE_PATH}", - "--exclude", - "*_qt.qt_event*", - "*_qt.containers.qt_tree*", - "*_qt.containers.qt_axis*", - "*components._viewer*", - "*components.viewer*", - "*components.dims*", - "*components.camera*", - "*components.LayerList", + f'{LAYER_LIST_MODULE_PATH}', + '--exclude', + '*_qt.qt_event*', + '*_qt.containers.qt_tree*', + '*_qt.containers.qt_axis*', + '*components._viewer*', + '*components.viewer*', + '*components.dims*', + '*components.camera*', + '*components.LayerList', # "napari.layers.*", - "--exclude-exact", - "napari._qt._qapp_model.qactions._debug", - "napari._qt._qapp_model.qactions._file", - "napari._qt._qapp_model.qactions._view", - "napari._qt._qapp_model.qactions._plugins", - "napari._qt._qapp_model.qactions._window", - "napari._qt._qapp_model.qactions._layers_actions", - "napari._qt._qapp_model.qactions._help", - "napari._qt.layer_controls", - "napari._qt.containers", - "napari.components", - "napari._qt", - "napari._qt._qapp_model.injection", - "--only", - "napari._qt", - "napari.components", - "napari.layers", - "--show-deps", - "--no-output", + '--exclude-exact', + 'napari._qt._qapp_model.qactions._debug', + 'napari._qt._qapp_model.qactions._file', + 'napari._qt._qapp_model.qactions._view', + 'napari._qt._qapp_model.qactions._plugins', + 'napari._qt._qapp_model.qactions._window', + 'napari._qt._qapp_model.qactions._layers_actions', + 'napari._qt._qapp_model.qactions._help', + 'napari._qt.layer_controls', + 'napari._qt.containers', + 'napari.components', + 'napari._qt', + 'napari._qt._qapp_model.injection', + '--only', + 'napari._qt', + 'napari.components', + 'napari.layers', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -514,37 +499,35 @@ def main(stubs=False): ) # ---- Layer controls section parameters - layer_controls_section_name = "Layers controls" - layer_controls_output_page = ( - UI_SECTIONS_DOCS_ROOT_PATH / "layers_controls_ui.md" - ) + layer_controls_section_name = 'Layers controls' + layer_controls_output_page = UI_SECTIONS_DOCS_ROOT_PATH / 'layers_controls_ui.md' layer_controls_pydeps_args = [ - f"{LAYER_CONTROLS_MODULE_PATH}", - "--exclude", - "*qt_event_loop", - "napari.layers.*", - "*components*", - "*qt_event_tracing*", - "*qt_event_filters*", - "*dialogs*", - "*app_model*", - "*utils", - "*status*", - "*thread*", - "*perf", - "*resources", - "*qplugins", - "--exclude-exact", - "napari._qt.widgets", - "--only", - "napari._qt", - "napari.components", - "napari.layers", - "napari.viewer", - "--max-bacon", - "3", - "--show-deps", - "--no-output", + f'{LAYER_CONTROLS_MODULE_PATH}', + '--exclude', + '*qt_event_loop', + 'napari.layers.*', + '*components*', + '*qt_event_tracing*', + '*qt_event_filters*', + '*dialogs*', + '*app_model*', + '*utils', + '*status*', + '*thread*', + '*perf', + '*resources', + '*qplugins', + '--exclude-exact', + 'napari._qt.widgets', + '--only', + 'napari._qt', + 'napari.components', + 'napari.layers', + 'napari.viewer', + '--max-bacon', + '3', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -556,34 +539,34 @@ def main(stubs=False): ) # ---- Application status bar section parameters - application_status_bar_section_name = "Application status bar" + application_status_bar_section_name = 'Application status bar' application_status_bar_output_page = ( - UI_SECTIONS_DOCS_ROOT_PATH / "application_status_bar_ui.md" + UI_SECTIONS_DOCS_ROOT_PATH / 'application_status_bar_ui.md' ) application_status_bar_pydeps_args = [ - f"{APPLICATION_STATUS_BAR_MODULE_PATH}", - "--exclude", - "*qt_viewer_dock_widget", - "napari._qt._qapp_model*", - "*qt_event_loop", - "*_qplugins", - "*utils", - "*qt_resources*", - "*preferences_dialog", - "*screenshot_dialog", - "*qt_viewer", - "*confirm_close_dialog", - "*qt_notification", - "--exclude-exact", - "napari._qt.threads", - "napari._qt.widgets", - "napari._qt.dialogs", - "--only", - "napari._qt", - "napari.utils.progress", - "napari.viewer", - "--show-deps", - "--no-output", + f'{APPLICATION_STATUS_BAR_MODULE_PATH}', + '--exclude', + '*qt_viewer_dock_widget', + 'napari._qt._qapp_model*', + '*qt_event_loop', + '*_qplugins', + '*utils', + '*qt_resources*', + '*preferences_dialog', + '*screenshot_dialog', + '*qt_viewer', + '*confirm_close_dialog', + '*qt_notification', + '--exclude-exact', + 'napari._qt.threads', + 'napari._qt.widgets', + 'napari._qt.dialogs', + '--only', + 'napari._qt', + 'napari.utils.progress', + 'napari.viewer', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -595,46 +578,46 @@ def main(stubs=False): ) # ---- Application menus section parameters - application_menus_section_name = "Application menus" + application_menus_section_name = 'Application menus' application_menus_output_page = ( - UI_SECTIONS_DOCS_ROOT_PATH / "application_menus_ui.md" + UI_SECTIONS_DOCS_ROOT_PATH / 'application_menus_ui.md' ) application_menus_pydeps_args = [ - f"{APPLICATION_MENUS_MODULE_PATH}", - "--exclude", - "*qt_event_loop*", - "*utils*", - "*qt_resources*", - "*containers*", - "*experimental*", - "*perf*", - "*qt_welcome*", - "*qt_viewer_dock_widget*", - "*qt_viewer_status_bar*", - "*qt_dims*", - "*_qplugins*", - "*qt_event_filters*", - "*threads*", - "*layer_controls*", - "*qt_notification*", - "*qt_activity_dialog*", - "*qt_progress_bar", - "*qt_spinbox", - "*qt_splash_screen", - "*qt_tooltip", - "--exclude-exact", - "napari._qt.widgets", - "napari._qt.dialogs", - "napari._qt._qapp_model", - "napari._qt._qapp_model.injection", - "napari._qt._qapp_model._menus", - "--only", - "napari._qt", - "napari.viewer", - "--max-bacon", - "3", - "--show-deps", - "--no-output", + f'{APPLICATION_MENUS_MODULE_PATH}', + '--exclude', + '*qt_event_loop*', + '*utils*', + '*qt_resources*', + '*containers*', + '*experimental*', + '*perf*', + '*qt_welcome*', + '*qt_viewer_dock_widget*', + '*qt_viewer_status_bar*', + '*qt_dims*', + '*_qplugins*', + '*qt_event_filters*', + '*threads*', + '*layer_controls*', + '*qt_notification*', + '*qt_activity_dialog*', + '*qt_progress_bar', + '*qt_spinbox', + '*qt_splash_screen', + '*qt_tooltip', + '--exclude-exact', + 'napari._qt.widgets', + 'napari._qt.dialogs', + 'napari._qt._qapp_model', + 'napari._qt._qapp_model.injection', + 'napari._qt._qapp_model._menus', + '--only', + 'napari._qt', + 'napari.viewer', + '--max-bacon', + '3', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -646,36 +629,36 @@ def main(stubs=False): ) # ---- Viewer section parameters - viewer_section_name = "Viewer" - viewer_output_page = UI_SECTIONS_DOCS_ROOT_PATH / "viewer_ui.md" + viewer_section_name = 'Viewer' + viewer_output_page = UI_SECTIONS_DOCS_ROOT_PATH / 'viewer_ui.md' viewer_pydeps_args = [ - f"{VIEWER_MODULE_PATH}", - "--exclude", - "*experimental*", - "*perf*", - "*plugins*", - "*Dims", - "*_qt.qt_event*", - "napari.layers.*", - "napari._qt.containers.*", - "napari._qt.layer_controls.*", - "napari._qt.dialogs.*", - "--exclude-exact", - "napari._qt._qapp_model", - "napari._qt.menus", - "napari._qt.qt_resources", - "napari._qt.widgets", - "napari._qt.widgets.qt_splash_screen", - "napari.components", - "napari._qt", - "--only", - "napari._qt", - "napari.components", - "napari.layers", - "--max-bacon", - "3", - "--show-deps", - "--no-output", + f'{VIEWER_MODULE_PATH}', + '--exclude', + '*experimental*', + '*perf*', + '*plugins*', + '*Dims', + '*_qt.qt_event*', + 'napari.layers.*', + 'napari._qt.containers.*', + 'napari._qt.layer_controls.*', + 'napari._qt.dialogs.*', + '--exclude-exact', + 'napari._qt._qapp_model', + 'napari._qt.menus', + 'napari._qt.qt_resources', + 'napari._qt.widgets', + 'napari._qt.widgets.qt_splash_screen', + 'napari.components', + 'napari._qt', + '--only', + 'napari._qt', + 'napari.components', + 'napari.layers', + '--max-bacon', + '3', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -687,49 +670,49 @@ def main(stubs=False): ) # ---- Dialogs section parameters - dialogs_section_name = "Dialogs" - dialogs_output_page = UI_SECTIONS_DOCS_ROOT_PATH / "dialogs_ui.md" + dialogs_section_name = 'Dialogs' + dialogs_output_page = UI_SECTIONS_DOCS_ROOT_PATH / 'dialogs_ui.md' dialogs_pydeps_args = [ - f"{DIALOGS_MODULE_PATH}", - "--exclude", - "*test*", - "*containers*", - "*experimental*", - "*layer_controls*", - "*perf*", - "*thread*", - "*qactions._window", - "*qactions._layerlist_context", - "*qactions._layers_actions", - "*qactions._view", - "*qactions._toggle_action", - "*splash*", - "*theme*", - "*keyboard*", - "*popup*", - "*event*", - "*dock_widget*", - "*welcome*", - "*viewer_buttons", - "*mode_buttons", - "*dict_table", - "*slider_compat", - "*size_preview", - "*darkdetect*", - "*resources*", - "--exclude-exact", - "napari._qt.dialogs", - "napari._vendor", - "napari._qt", - "napari._qt.widgets", - "napari._qt._qapp_model", - "napari._qt._qapp_model.injection", - "napari._qt._qplugins", - "--only", - "napari._qt", - "napari._vendor", - "--show-deps", - "--no-output", + f'{DIALOGS_MODULE_PATH}', + '--exclude', + '*test*', + '*containers*', + '*experimental*', + '*layer_controls*', + '*perf*', + '*thread*', + '*qactions._window', + '*qactions._layerlist_context', + '*qactions._layers_actions', + '*qactions._view', + '*qactions._toggle_action', + '*splash*', + '*theme*', + '*keyboard*', + '*popup*', + '*event*', + '*dock_widget*', + '*welcome*', + '*viewer_buttons', + '*mode_buttons', + '*dict_table', + '*slider_compat', + '*size_preview', + '*darkdetect*', + '*resources*', + '--exclude-exact', + 'napari._qt.dialogs', + 'napari._vendor', + 'napari._qt', + 'napari._qt.widgets', + 'napari._qt._qapp_model', + 'napari._qt._qapp_model.injection', + 'napari._qt._qplugins', + '--only', + 'napari._qt', + 'napari._vendor', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -741,22 +724,22 @@ def main(stubs=False): ) # ---- Napari-console section parameters - console_section_name = "Console (napari-console)" - console_output_page = UI_SECTIONS_DOCS_ROOT_PATH / "console_ui.md" + console_section_name = 'Console (napari-console)' + console_output_page = UI_SECTIONS_DOCS_ROOT_PATH / 'console_ui.md' console_pydeps_args = [ - f"{VIEWER_MODULE_PATH}", - "--only", - "napari.components._viewer_key_bindings", - "napari.components.viewer", - "napari.viewer", - "napari.utils.notifications", - "napari._qt.qt_viewer", - "napari._qt.widgets.qt_viewer_buttons", - "napari._qt._qapp_model.qactions._window", - "napari._qt.qt_main_window", - "napari_console", - "--show-deps", - "--no-output", + f'{VIEWER_MODULE_PATH}', + '--only', + 'napari.components._viewer_key_bindings', + 'napari.components.viewer', + 'napari.viewer', + 'napari.utils.notifications', + 'napari._qt.qt_viewer', + 'napari._qt.widgets.qt_viewer_buttons', + 'napari._qt._qapp_model.qactions._window', + 'napari._qt.qt_main_window', + 'napari_console', + '--show-deps', + '--no-output', ] ui_sections.append( ( @@ -778,8 +761,8 @@ def main(stubs=False): if not output_page.exists(): # Avoid overwriting existing files output_page.parent.mkdir(exist_ok=True, parents=True) output_page.write_text( - f"## {section_name}\nThis is a stub. The real file is autogenerated in a full build.", - encoding="utf-8", + f'## {section_name}\nThis is a stub. The real file is autogenerated in a full build.', + encoding='utf-8', ) else: # Generate full content @@ -791,14 +774,14 @@ def main(stubs=False): ) -if __name__ == "__main__": +if __name__ == '__main__': import argparse - parser = argparse.ArgumentParser(description="Update UI sections docs.") + parser = argparse.ArgumentParser(description='Update UI sections docs.') parser.add_argument( - "--stubs", - action="store_true", - help="Generate stubs versions of the UI section docs.", + '--stubs', + action='store_true', + help='Generate stubs versions of the UI section docs.', ) args = parser.parse_args() diff --git a/docs/_static/custom.css b/docs/_static/custom.css index eb3824c94..5c54ad811 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -41,4 +41,4 @@ div.sphx-glr-download a:hover { border-color: var(--pst-color-secondary-bg); color: var(--napari-color-text-base); font-weight: 700; -} \ No newline at end of file +} diff --git a/docs/_templates/details_polygon_path_tool.md b/docs/_templates/details_polygon_path_tool.md index ddcece7b9..502b502f9 100644 --- a/docs/_templates/details_polygon_path_tool.md +++ b/docs/_templates/details_polygon_path_tool.md @@ -1,44 +1,43 @@ -The sequence of events to draw a polygon or path are almost the same +The sequence of events to draw a polygon or path are almost the same when using either the mouse or a tablet. Drawing with mouse 1. Click mouse (left-click) to begin drawing. -2. Move mouse -- without holding down the mouse button -- to draw the path. -3. Click mouse (left-click) or press `Esc` to end drawing the path or polygon. +1. Move mouse -- without holding down the mouse button -- to draw the path. +1. Click mouse (left-click) or press `Esc` to end drawing the path or polygon. In case of drawing a polygon the polygon will be automatically completed. - Drawing with tablet -The polygon lasso and the path tool can also be used to draw `Polygons` or `Paths` -using a tablet. In this case, drawing the polygon or path is started by touching -the tablet screen with the tablet stylus and drawing will continue for as long -as the pencil is moved while touching the tablet screen. Note that similar behavior +The polygon lasso and the path tool can also be used to draw `Polygons` or `Paths` +using a tablet. In this case, drawing the polygon or path is started by touching +the tablet screen with the tablet stylus and drawing will continue for as long +as the pencil is moved while touching the tablet screen. Note that similar behavior is also available when using a macOS trackpad, using three-finger drag mode. Adding of vertices while drawing -For both mouse and tablet mode, vertices are added only if the vertex to be added -is at a certain number of screen pixels away from the previous vertex. This value +For both mouse and tablet mode, vertices are added only if the vertex to be added +is at a certain number of screen pixels away from the previous vertex. This value can be adjusted in the settings in napari by going to `File` -> `Preferences` (or -`control + shift + p`), then in the menu on the left-clicking on `Experimental` -and then adjusting the value of `Minimum distance threshold of shapes lasso tool`. -The default is 10 and can be any integer higher than 0 and lower than 50. As with -the polygon creation tool, drawing the shape can also be finished by pressing the +`control + shift + p`), then in the menu on the left-clicking on `Experimental` +and then adjusting the value of `Minimum distance threshold of shapes lasso tool`. +The default is 10 and can be any integer higher than 0 and lower than 50. As with +the polygon creation tool, drawing the shape can also be finished by pressing the `Esc` key. Reducing the number of vertices -After finishing drawing a polygon or path, an implementation of the -[Ramer–Douglas–Peucker algorithm](https://en.wikipedia.org/wiki/Ramer–Douglas–Peucker_algorithm) +After finishing drawing a polygon or path, an implementation of the +[Ramer–Douglas–Peucker algorithm](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm) is applied to reduce the number of vertices that make up the geometry. In case of the path the structure is preserved while in case of a polygon the contour is preserved. -The aggressiveness with which the algorithm reduces the number of vertices is determined -by an `epsilon` parameter, which is a perpendicular distance threshold. Any vertices -beyond the threshold will be preserved, so if `epsilon` is set to `0`, no vertices -will be removed. With increasing values of `epsilon`, more and more vertices will +The aggressiveness with which the algorithm reduces the number of vertices is determined +by an `epsilon` parameter, which is a perpendicular distance threshold. Any vertices +beyond the threshold will be preserved, so if `epsilon` is set to `0`, no vertices +will be removed. With increasing values of `epsilon`, more and more vertices will be removed. The value of `epsilon` can be set in napari by going to `File` -> -`Preferences` (or `control + shift + p`), then in the menu on the left-clicking -on `Experimental` and then adjusting the value of `RDP epsilon`. The default value -is 0.5 and cannot be set lower than 0. \ No newline at end of file +`Preferences` (or `control + shift + p`), then in the menu on the left-clicking +on `Experimental` and then adjusting the value of `RDP epsilon`. The default value +is 0.5 and cannot be set lower than 0. diff --git a/docs/_templates/sbt-sidebar-footer.html b/docs/_templates/sbt-sidebar-footer.html index 386a86cc4..74768b72e 100644 --- a/docs/_templates/sbt-sidebar-footer.html +++ b/docs/_templates/sbt-sidebar-footer.html @@ -3,4 +3,4 @@ -{% endif %} \ No newline at end of file +{% endif %} diff --git a/docs/api/index.md b/docs/api/index.md index b057a5a33..52756e979 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,7 +1,8 @@ (api)= + # API Reference Information on specific functions, classes, and methods. ```{tableofcontents} -``` \ No newline at end of file +``` diff --git a/docs/community/code_of_conduct.md b/docs/community/code_of_conduct.md index 913dc0dcb..9b732abbd 100644 --- a/docs/community/code_of_conduct.md +++ b/docs/community/code_of_conduct.md @@ -1,4 +1,5 @@ (napari-coc)= + # Code of Conduct ## Introduction @@ -15,25 +16,25 @@ We strive to: 1. Be open. We invite anyone to participate in our community. We prefer to use public methods of communication for project-related messages, unless discussing something sensitive. This applies to messages for help or project-related support, too; not only is a public support request much more likely to result in an answer to a question, it also ensures that any inadvertent mistakes in answering are more easily detected and corrected. -2. Be empathetic, welcoming, friendly, and patient. We work together to resolve conflict, and assume good intentions. We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal attack. A community where people feel uncomfortable or threatened is not a productive one. +1. Be empathetic, welcoming, friendly, and patient. We work together to resolve conflict, and assume good intentions. We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal attack. A community where people feel uncomfortable or threatened is not a productive one. -3. Be collaborative. Our work will be used by other people, and in turn we will depend on the work of others. When we make something for the benefit of the project, we are willing to explain to others how it works, so that they can build on the work to make it even better. Any decision we make will affect users and colleagues, and we take those consequences seriously when making decisions. +1. Be collaborative. Our work will be used by other people, and in turn we will depend on the work of others. When we make something for the benefit of the project, we are willing to explain to others how it works, so that they can build on the work to make it even better. Any decision we make will affect users and colleagues, and we take those consequences seriously when making decisions. -4. Be inquisitive. Nobody knows everything! Asking questions early avoids many problems later, so we encourage questions, although we may direct them to the appropriate forum. We will try hard to be responsive and helpful. +1. Be inquisitive. Nobody knows everything! Asking questions early avoids many problems later, so we encourage questions, although we may direct them to the appropriate forum. We will try hard to be responsive and helpful. -5. Be careful in the words that we choose. We are careful and respectful in our communication and we take responsibility for our own speech. Be kind to others. Do not insult or put down other participants. We will not accept harassment or other exclusionary behaviour, such as: +1. Be careful in the words that we choose. We are careful and respectful in our communication and we take responsibility for our own speech. Be kind to others. Do not insult or put down other participants. We will not accept harassment or other exclusionary behaviour, such as: -> * Violent threats or language directed against another person. -> * Sexist, racist, ableist, or otherwise discriminatory jokes and language. -> * Posting sexually explicit or violent material. -> * Posting (or threatening to post) other people’s personally identifying information (“doxing”). -> * Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history, without the sender’s consent. -> * Personal insults, especially those using sexist, racist, or ableist terms. -> * Intentional or repeated misgendering of participants who have explicitly requested to be addressed by specific pronouns. -> * Unwelcome sexual attention. -> * Excessive profanity. Please avoid swearwords; people differ greatly in their sensitivity to swearing. -> * Repeated harassment of others. In general, if someone asks you to stop, then stop. -> * Advocating for, or encouraging, any of the above behaviour. +> - Violent threats or language directed against another person. +> - Sexist, racist, ableist, or otherwise discriminatory jokes and language. +> - Posting sexually explicit or violent material. +> - Posting (or threatening to post) other people’s personally identifying information (“doxing”). +> - Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history, without the sender’s consent. +> - Personal insults, especially those using sexist, racist, or ableist terms. +> - Intentional or repeated misgendering of participants who have explicitly requested to be addressed by specific pronouns. +> - Unwelcome sexual attention. +> - Excessive profanity. Please avoid swearwords; people differ greatly in their sensitivity to swearing. +> - Repeated harassment of others. In general, if someone asks you to stop, then stop. +> - Advocating for, or encouraging, any of the above behaviour. ## Diversity statement @@ -53,14 +54,14 @@ For clearly intentional breaches, report those to the Code of Conduct committee You can report issues to the napari Code of Conduct committee, at napari-conduct@googlegroups.com . Currently, the committee consists of: -* [Juan Nunez-Iglesias](https://github.com/jni) (chair) -* [Draga Doncila Pop](https://github.com/DragaDoncila) -* [Peter Sobolewski](https://github.com/psobolewskiPhD) -* [Tim Monko](https://github.com/TimMonko) +- [Juan Nunez-Iglesias](https://github.com/jni) (chair) +- [Draga Doncila Pop](https://github.com/DragaDoncila) +- [Peter Sobolewski](https://github.com/psobolewskiPhD) +- [Tim Monko](https://github.com/TimMonko) If your report involves any members of the committee, or if they feel they have a conflict of interest in handling it, then they will recuse themselves from considering your report. Alternatively, if for any reason you feel uncomfortable making a report to the committee, then you can also contact: -* [Carol Willing](https://github.com/willingc) at willingc@gmail.com +- [Carol Willing](https://github.com/willingc) at willingc@gmail.com ## Incident reporting resolution & Code of Conduct enforcement @@ -74,11 +75,11 @@ In cases not involving clear severe and obvious breaches of this code of conduct 1. acknowledge report is received -2. reasonable discussion/feedback +1. reasonable discussion/feedback -3. mediation (if feedback didn’t help, and only if both reporter and reportee agree to this) +1. mediation (if feedback didn’t help, and only if both reporter and reportee agree to this) -4. enforcement via transparent decision (see [Resolutions](code_of_conduct_reporting.md#resolutions)) by the Code of Conduct Committee +1. enforcement via transparent decision (see [Resolutions](code_of_conduct_reporting.md#resolutions)) by the Code of Conduct Committee The committee will respond to any report as soon as possible, and at most within 72 hours. @@ -86,5 +87,5 @@ The committee will respond to any report as soon as possible, and at most within We are thankful to the groups behind the following documents, from which we drew content and inspiration: -* [The SciPy Code of Conduct](https://docs.scipy.org/doc/scipy/dev/conduct/code_of_conduct.html) -* [The Numpy Code of Conduct](https://numpy.org/code-of-conduct/) +- [The SciPy Code of Conduct](https://docs.scipy.org/doc/scipy/dev/conduct/code_of_conduct.html) +- [The Numpy Code of Conduct](https://numpy.org/code-of-conduct/) diff --git a/docs/community/code_of_conduct_reporting.md b/docs/community/code_of_conduct_reporting.md index e93cb64a0..2bc666b31 100644 --- a/docs/community/code_of_conduct_reporting.md +++ b/docs/community/code_of_conduct_reporting.md @@ -1,37 +1,38 @@ (coc-reporting)= + # Handling Code of Conduct reports This is the manual followed by napari's Code of Conduct Committee. It’s used when we respond to an issue to make sure we’re consistent and fair. Enforcing the Code of Conduct impacts our community today and for the future. It’s an action that we do not take lightly. When reviewing enforcement measures, the Code of Conduct Committee will keep the following values and guidelines in mind: -* Act in a personal manner rather than impersonal. The Committee can engage the parties to understand the situation, while respecting the privacy and any necessary confidentiality of reporters. However, sometimes it is necessary to communicate with one or more individuals directly: the Committee’s goal is to improve the health of our community rather than only produce a formal decision. +- Act in a personal manner rather than impersonal. The Committee can engage the parties to understand the situation, while respecting the privacy and any necessary confidentiality of reporters. However, sometimes it is necessary to communicate with one or more individuals directly: the Committee’s goal is to improve the health of our community rather than only produce a formal decision. -* Emphasize empathy for individuals rather than judging behavior, avoiding binary labels of “good” and “bad/evil”. Overt, clear-cut aggression and harassment exists and we will be address that firmly. But many scenarios that can prove challenging to resolve are those where normal disagreements devolve into unhelpful or harmful behavior from multiple parties. Understanding the full context and finding a path that re-engages all is hard, but ultimately the most productive for our community. +- Emphasize empathy for individuals rather than judging behavior, avoiding binary labels of “good” and “bad/evil”. Overt, clear-cut aggression and harassment exists and we will be address that firmly. But many scenarios that can prove challenging to resolve are those where normal disagreements devolve into unhelpful or harmful behavior from multiple parties. Understanding the full context and finding a path that re-engages all is hard, but ultimately the most productive for our community. -* We understand that email is a difficult medium and can be isolating. Receiving criticism over email, without personal contact, can be particularly painful. This makes it especially important to keep an atmosphere of open-minded respect of the views of others. It also means that we must be transparent in our actions, and that we will do everything in our power to make sure that all our members are treated fairly and with sympathy. +- We understand that email is a difficult medium and can be isolating. Receiving criticism over email, without personal contact, can be particularly painful. This makes it especially important to keep an atmosphere of open-minded respect of the views of others. It also means that we must be transparent in our actions, and that we will do everything in our power to make sure that all our members are treated fairly and with sympathy. -* Discrimination can be subtle and it can be unconscious. It can show itself as unfairness and hostility in otherwise ordinary interactions. We know that this does occur, and we will take care to look out for it. We would very much like to hear from you if you feel you have been treated unfairly, and we will use these procedures to make sure that your complaint is heard and addressed. +- Discrimination can be subtle and it can be unconscious. It can show itself as unfairness and hostility in otherwise ordinary interactions. We know that this does occur, and we will take care to look out for it. We would very much like to hear from you if you feel you have been treated unfairly, and we will use these procedures to make sure that your complaint is heard and addressed. -* Help increase engagement in good discussion practice: try to identify where discussion may have broken down and provide actionable information, pointers and resources that can lead to positive change on these points. +- Help increase engagement in good discussion practice: try to identify where discussion may have broken down and provide actionable information, pointers and resources that can lead to positive change on these points. -* Be mindful of the needs of new members: provide them with explicit support and consideration, with the aim of increasing participation from underrepresented groups in particular. +- Be mindful of the needs of new members: provide them with explicit support and consideration, with the aim of increasing participation from underrepresented groups in particular. -* Individuals come from different cultural backgrounds and native languages. Try to identify any honest misunderstandings caused by a non-native speaker and help them understand the issue and what they can change to avoid causing offence. Complex discussion in a foreign language can be very intimidating, and we want to grow our diversity also across nationalities and cultures. +- Individuals come from different cultural backgrounds and native languages. Try to identify any honest misunderstandings caused by a non-native speaker and help them understand the issue and what they can change to avoid causing offence. Complex discussion in a foreign language can be very intimidating, and we want to grow our diversity also across nationalities and cultures. *Mediation*: voluntary, informal mediation is a tool at our disposal. In contexts such as when two or more parties have all escalated to the point of inappropriate behavior (something sadly common in human conflict), it may be useful to facilitate a mediation process. This is only an example: the Committee can consider mediation in any case, mindful that the process is meant to be strictly voluntary and no party can be pressured to participate. If the Committee suggests mediation, it should: -* Find a candidate who can serve as a mediator. +- Find a candidate who can serve as a mediator. -* Obtain the agreement of the reporter(s). The reporter(s) have complete freedom to decline the mediation idea, or to propose an alternate mediator. +- Obtain the agreement of the reporter(s). The reporter(s) have complete freedom to decline the mediation idea, or to propose an alternate mediator. -* Obtain the agreement of the reported person(s). +- Obtain the agreement of the reported person(s). -* Settle on the mediator: while parties can propose a different mediator than the suggested candidate, only if common agreement is reached on all terms can the process move forward. +- Settle on the mediator: while parties can propose a different mediator than the suggested candidate, only if common agreement is reached on all terms can the process move forward. -* Establish a timeline for mediation to complete, ideally within two weeks. +- Establish a timeline for mediation to complete, ideally within two weeks. -* The mediator will engage with all the parties and seek a resolution that is satisfactory to all. Upon completion, the mediator will provide a report (vetted by all parties to the process) to the Committee, with recommendations on further steps. The Committee will then evaluate these results (whether satisfactory resolution was achieved or not) and decide on any additional action deemed necessary. +- The mediator will engage with all the parties and seek a resolution that is satisfactory to all. Upon completion, the mediator will provide a report (vetted by all parties to the process) to the Committee, with recommendations on further steps. The Committee will then evaluate these results (whether satisfactory resolution was achieved or not) and decide on any additional action deemed necessary. ## How the committee will respond to reports @@ -43,13 +44,13 @@ We know that it is painfully common for internet communication to start at or de When a member of the Code of Conduct committee becomes aware of a clear and severe breach, they will do the following: -* Immediately disconnect the originator from all napari communication channels. +- Immediately disconnect the originator from all napari communication channels. -* Reply to the reporter that their report has been received and that the originator has been disconnected. +- Reply to the reporter that their report has been received and that the originator has been disconnected. -* In every case, the moderator should make a reasonable effort to contact the originator, and tell them specifically how their language or actions qualify as a “clear and severe breach”. The moderator should also say that, if the originator believes this is unfair or they want to be reconnected to napari, they have the right to ask for a review, as below, by the Code of Conduct Committee. The moderator should copy this explanation to the Code of Conduct Committee. +- In every case, the moderator should make a reasonable effort to contact the originator, and tell them specifically how their language or actions qualify as a “clear and severe breach”. The moderator should also say that, if the originator believes this is unfair or they want to be reconnected to napari, they have the right to ask for a review, as below, by the Code of Conduct Committee. The moderator should copy this explanation to the Code of Conduct Committee. -* The Code of Conduct Committee will formally review and sign off on all cases where this mechanism has been applied to make sure it is not being used to control ordinary heated disagreement. +- The Code of Conduct Committee will formally review and sign off on all cases where this mechanism has been applied to make sure it is not being used to control ordinary heated disagreement. ### Report handling @@ -59,13 +60,13 @@ If a report doesn’t contain enough information, the committee will obtain all The committee will then review the incident and determine, to the best of their ability: -* What happened. +- What happened. -* Whether this event constitutes a Code of Conduct violation. +- Whether this event constitutes a Code of Conduct violation. -* Who are the responsible party(ies). +- Who are the responsible party(ies). -* Whether this is an ongoing situation, and there is a threat to anyone’s physical safety. +- Whether this is an ongoing situation, and there is a threat to anyone’s physical safety. This information will be collected in writing, and whenever possible the group’s deliberations will be recorded and retained (i.e. chat transcripts, email discussions, recorded conference calls, summaries of voice conversations, etc). @@ -79,25 +80,25 @@ The committee must agree on a resolution by consensus. If the group cannot reach Possible responses may include: -* Taking no further action +- Taking no further action - * if we determine no violations have occurred. + - if we determine no violations have occurred. - * if the matter has been resolved publicly while the committee was considering responses. + - if the matter has been resolved publicly while the committee was considering responses. -* Coordinating voluntary mediation: if all involved parties agree, the Committee may facilitate a mediation process as detailed above. +- Coordinating voluntary mediation: if all involved parties agree, the Committee may facilitate a mediation process as detailed above. -* Remind publicly, and point out that some behavior/actions/language have been judged inappropriate and why in the current context, or can but hurtful to some people, requesting the community to self-adjust. +- Remind publicly, and point out that some behavior/actions/language have been judged inappropriate and why in the current context, or can but hurtful to some people, requesting the community to self-adjust. -* A private reprimand from the committee to the individual(s) involved. In this case, the group chair will deliver that reprimand to the individual(s) over email, cc’ing the group. +- A private reprimand from the committee to the individual(s) involved. In this case, the group chair will deliver that reprimand to the individual(s) over email, cc’ing the group. -* A public reprimand. In this case, the committee chair will deliver that reprimand in the same venue that the violation occurred, within the limits of practicality. E.g., the original mailing list for an email violation, but for a chat room discussion where the person/context may be gone, they can be reached by other means. The group may choose to publish this message elsewhere for documentation purposes. +- A public reprimand. In this case, the committee chair will deliver that reprimand in the same venue that the violation occurred, within the limits of practicality. E.g., the original mailing list for an email violation, but for a chat room discussion where the person/context may be gone, they can be reached by other means. The group may choose to publish this message elsewhere for documentation purposes. -* A request for a public or private apology, assuming the reporter agrees to this idea: they may at their discretion refuse further contact with the violator. The chair will deliver this request. The committee may, if it chooses, attach “strings” to this request: for example, the group may ask a violator to apologize in order to retain one’s membership on a mailing list. +- A request for a public or private apology, assuming the reporter agrees to this idea: they may at their discretion refuse further contact with the violator. The chair will deliver this request. The committee may, if it chooses, attach “strings” to this request: for example, the group may ask a violator to apologize in order to retain one’s membership on a mailing list. -* A “mutually agreed upon hiatus” where the committee asks the individual to temporarily refrain from community participation. If the individual chooses not to take a temporary break voluntarily, the committee may issue a “mandatory cooling off period”. +- A “mutually agreed upon hiatus” where the committee asks the individual to temporarily refrain from community participation. If the individual chooses not to take a temporary break voluntarily, the committee may issue a “mandatory cooling off period”. -* A permanent or temporary ban from some or all napari spaces (mailing lists, slack, etc.). The group will maintain records of all such bans so that they may be reviewed in the future or otherwise maintained. +- A permanent or temporary ban from some or all napari spaces (mailing lists, slack, etc.). The group will maintain records of all such bans so that they may be reviewed in the future or otherwise maintained. Once a resolution is agreed upon, but before it is enacted, the committee will contact the original reporter and any other affected parties and explain the proposed resolution. The committee will ask if this resolution is acceptable, and must note feedback for the record. diff --git a/docs/community/governance.md b/docs/community/governance.md index 5f816980a..3e5b78b55 100644 --- a/docs/community/governance.md +++ b/docs/community/governance.md @@ -1,4 +1,5 @@ (napari-governance)= + # Governance model ## Abstract @@ -68,11 +69,11 @@ activities. Core team members are expected to review code contributions while adhering to the [core team member guide](core-dev-guide). New core team members can be nominated by any existing core team member, and for details on that process see our core -team member guide. Core team members can choose to step down from their role and +team member guide. Core team members can choose to step down from their role and become "emeritus" core team members if they are no longer involved with the project, or feel they do not have sufficient time to dedicate to their role. Emeritus core team members can request or be invited to become active core team members at -a later date and with consensus from currently active core team members. +a later date and with consensus from currently active core team members. For a full list of core team members see our [About the project and team](team) page. @@ -111,7 +112,7 @@ then admission to the SC by consensus. During that time deadlocked votes of the be postponed until the new member has joined and another vote can be held. The IFP seat is elected by the IFP Advisory Council. -The SC may be contacted at `napari-steering-council@googlegroups.com`. For a list of the current +The SC may be contacted at `napari-steering-council@googlegroups.com`. For a list of the current SC see our [About the project and team](team) page. ### Institutional and funding partners @@ -155,9 +156,9 @@ IFP benefits are: - Acknowledgement on the napari website, including homepage, and in talks. - Ability to acknowledge their contribution to napari on their own websites and in talks. - Ability to provide input to the project through their Institutional Partner -Representative. + Representative. - Ability to influence the project through the election of the Institutional -and Funding Partner seat on the SC. + and Funding Partner seat on the SC. For a full list of current IFPs and their Representatives see our [About Us](team) page. @@ -177,7 +178,7 @@ napari uses a “consensus seeking” process for making decisions. The group tries to find a resolution that has no open objections among core team members. Core team members are expected to distinguish between fundamental objections to a proposal and minor perceived flaws that they can live with, and not hold up the -decision-making process for the latter. If no option can be found without +decision-making process for the latter. If no option can be found without objections, the decision is escalated to the SC, which will itself use consensus seeking to come to a resolution. In the unlikely event that there is still a deadlock, the proposal will move forward if it has the support of a @@ -204,11 +205,11 @@ are made according to the following rules: decision-making process outlined above. - **Changes to this governance model or our mission, vision, and values** - require a dedicated issue on our [issue tracker](https://github.com/napari/napari/issues) + require a dedicated issue on our [issue tracker](https://github.com/napari/napari/issues) and follow the decision-making process outlined above, *unless* there is unanimous agreement from core team members on the change in which case it can move forward faster. If an objection is raised on a lazy consensus, the proposer can appeal to the community and core team members and the change can be approved or rejected by -escalating to the SC. \ No newline at end of file +escalating to the SC. diff --git a/docs/community/index.md b/docs/community/index.md index 1b804789f..4ea5dd15a 100644 --- a/docs/community/index.md +++ b/docs/community/index.md @@ -1,4 +1,5 @@ (community)= + # Community There are several different ways to be a part of the napari community. From @@ -18,19 +19,20 @@ you can visit ![forum image.sc logo](../images/image_sc_logo.png) [forum.image.s To get the latest news from the napari team, follow us on: - * ![mastodon logo](../images/mastodon_logo.svg) mastodon [@napari@fosstodon.org](https://fosstodon.org/@napari) - * ![bluesky logo](../images/Bluesky_Logo.svg) BlueSky [@napari.org](https://bsky.app/profile/napari.org) +- ![mastodon logo](../images/mastodon_logo.svg) mastodon [@napari@fosstodon.org](https://fosstodon.org/@napari) +- ![bluesky logo](../images/Bluesky_Logo.svg) BlueSky [@napari.org](https://bsky.app/profile/napari.org) ## Contributing and Bug reporting -The napari development happens in the napari GitHub repository [github.com/napari/napari](https://github.com/napari/napari). -If you spot a bug, please check our [issues list](https://github.com/napari/napari/issues) . If you -do not find a matching report, please open a new issue. +The napari development happens in the napari GitHub repository [github.com/napari/napari](https://github.com/napari/napari). +If you spot a bug, please check our [issues list](https://github.com/napari/napari/issues) . If you +do not find a matching report, please open a new issue. If you are interested in contributing, check out our [contributing guide](napari-contributing). ## Blog + To read announcements, learn more about who is using napari and see what our community has to say, check out our blog, the [Island Dispatch](https://napari.org/island-dispatch). diff --git a/docs/community/licensing.md b/docs/community/licensing.md index 979f121b0..a909d4a8c 100644 --- a/docs/community/licensing.md +++ b/docs/community/licensing.md @@ -4,7 +4,9 @@ napari is distributed under the BSD-3-Clause license, a copy of which is availab [here](https://github.com/napari/napari/blob/latest/LICENSE). ```{include} ../../LICENSE -:literal: true +--- +literal: true +--- ``` ```{include} ../../EULA.md diff --git a/docs/community/meeting_schedule.md b/docs/community/meeting_schedule.md index b58f1b3d9..979187c50 100644 --- a/docs/community/meeting_schedule.md +++ b/docs/community/meeting_schedule.md @@ -23,7 +23,9 @@ If you are using napari or interested in how napari could be used in your work, + +