From baeb2170b23acf60a3b2608212918068bd3a94db Mon Sep 17 00:00:00 2001 From: Nick Hodgskin <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:30:24 +0200 Subject: [PATCH 1/2] MAINT: Add script for numpydoc linting of Public API (#11121) --- ci/numpydoc-public-api.py | 175 ++++++++++++++++++++++++++++++++++++++ doc/whats-new.rst | 2 + pixi.toml | 7 ++ 3 files changed, 184 insertions(+) create mode 100644 ci/numpydoc-public-api.py diff --git a/ci/numpydoc-public-api.py b/ci/numpydoc-public-api.py new file mode 100644 index 00000000000..b044bf8bb19 --- /dev/null +++ b/ci/numpydoc-public-api.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +"""A script that can be quickly run that explores the public API of Xarray +and validates docstrings along the way according to the numpydoc conventions.""" + +import functools +import importlib +import logging +import sys +import types +from pathlib import Path + +from numpydoc.validate import validate + +logger = logging.getLogger("numpydoc-public-api") +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) +logger.addHandler(handler) + +PROJECT_ROOT = (Path(__file__).parent / "..").resolve() +PUBLIC_MODULES = ["xarray"] +ROOT_PACKAGE = "xarray" + +# full list of numpydoc error codes: https://numpydoc.readthedocs.io/en/latest/validation.html +SKIP_ERRORS = [ # TODO: Curate these for Xarray + "GL01", # parcels is fine with the summary line starting directly after `"""`, or on the next line. + "SA01", # Parcels doesn't require the "See also" section + "SA04", + "ES01", # We don't require the extended summary for all docstrings + "EX01", # We don't require the "Examples" section for all docstrings + "SS06", # Not possible to make all summaries one line + # + # To be fixed up + "GL02", # Closing quotes should be placed in the line after the last text in the docstring (do not close the quotes in the same line as the text, or leave a blank line between the last text and the quotes) + "GL03", # Double line break found; please use only one blank line to separate sections or paragraphs, and do not leave blank lines at the end of docstrings + "GL07", # Sections are in the wrong order. Correct order is: {correct_sections} + "GL08", # The object does not have a docstring + "SS01", # No summary found (a short summary in a single line should be present at the beginning of the docstring) + "SS02", # Summary does not start with a capital letter + "SS03", # Summary does not end with a period + "SS04", # Summary contains heading whitespaces + "SS05", # Summary must start with infinitive verb, not third person (e.g. use "Generate" instead of "Generates") + "PR01", # Parameters {missing_params} not documented + "PR02", # Unknown parameters {unknown_params} + "PR03", # Wrong parameters order. Actual: {actual_params}. Documented: {documented_params} + "SA02", # Missing period at end of description for See Also "{reference_name}" reference + "SA03", # Description should be capitalized for See Also + # + # TODO consider whether to continue ignoring the following + "GL09", # Deprecation warning should precede extended summary + "GL10", # reST directives {directives} must be followed by two colons + "PR04", # Parameter "{param_name}" has no type + "PR05", # Parameter "{param_name}" type should not finish with "." + "PR06", # Parameter "{param_name}" type should use "{right_type}" instead of "{wrong_type}" + "PR07", # Parameter "{param_name}" has no description + "PR08", # Parameter "{param_name}" description should start with a capital letter + "PR09", # Parameter "{param_name}" description should finish with "." + "PR10", # Parameter "{param_name}" requires a space before the colon separating the parameter name and type + "RT01", # No Returns section found + "RT02", # The first line of the Returns section should contain only the type, unless multiple values are being returned + "RT03", # Return value has no description + "RT04", # Return value description should start with a capital letter + "RT05", # Return value description should finish with "." + "YD01", # No Yields section found +] + + +def is_built_in(type_or_instance: type | object): + if isinstance(type_or_instance, type): + return type_or_instance.__module__ == "builtins" + else: + return type_or_instance.__class__.__module__ == "builtins" + + +def walk_module(module_str: str, public_api: list[str] | None = None) -> list[str]: + if public_api is None: + public_api = [] + + module = importlib.import_module(module_str) + try: + all_ = module.__all__ + except AttributeError: + print(f"No __all__ variable found in public module {module_str!r}") + return public_api + + if module_str not in public_api: + public_api.append(module_str) + for item_str in all_: + item = getattr(module, item_str) + if isinstance(item, types.ModuleType): + walk_module(f"{module_str}.{item_str}", public_api) + if isinstance(item, (types.FunctionType,)): + public_api.append(f"{module_str}.{item_str}") + elif is_built_in(item): + print(f"Found builtin at '{module_str}.{item_str}' of type {type(item)}") + continue + elif isinstance(item, type): + public_api.append(f"{module_str}.{item_str}") + walk_class(module_str, item, public_api) + else: + logger.info( + f"Encountered unexpected public object at '{module_str}.{item_str}' of {item!r} in public API. Don't know how to handle with numpydoc - ignoring." + ) + + return public_api + + +def get_public_class_attrs(class_: type) -> set[str]: + return {a for a in dir(class_) if not a.startswith("_")} + + +def walk_class(module_str: str, class_: type, public_api: list[str]) -> list[str]: + class_str = class_.__name__ + + # attributes that were introduced by this class specifically - not from inheritance + attrs = get_public_class_attrs(class_) - functools.reduce( + set.union, (get_public_class_attrs(base) for base in class_.__bases__) + ) + + public_api.extend([f"{module_str}.{class_str}.{attr_str}" for attr_str in attrs]) + return public_api + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Validate numpydoc docstrings in the public API" + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Increase verbosity (can be repeated)", + ) + args = parser.parse_args() + + # Set logging level based on verbosity: 0=WARNING, 1=INFO, 2+=DEBUG + if args.verbose == 0: + log_level = logging.WARNING + elif args.verbose == 1: + log_level = logging.INFO + else: + log_level = logging.DEBUG + + logger.setLevel(log_level) + + public_api = [] + for module in PUBLIC_MODULES: + public_api += walk_module(module) + + errors = 0 + for item in public_api: + logger.info(f"Processing validating {item}") + try: + res = validate(item) + except (AttributeError, StopIteration, ValueError) as e: + if ( + isinstance(e, ValueError) and "Error parsing See Also entry" in str(e) + ): # TODO: Fix later https://github.com/pydata/xarray/issues/8596#issuecomment-3832443795 + logger.info(f"Skipping See Also parsing error for {item!r}.") + continue + logger.warning(f"Could not process {item!r}. Encountered error. {e!r}") + continue + if res["type"] in ("module", "float", "int", "dict"): + continue + for err in res["errors"]: + if err[0] not in SKIP_ERRORS: + print(f"{item}: {err}") + errors += 1 + sys.exit(errors) + + +if __name__ == "__main__": + main() diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 5b711dcfd37..6c7680f6619 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -134,6 +134,8 @@ Performance Internal Changes ~~~~~~~~~~~~~~~~ +- Add script for linting of public docstrings according to numpydoc (:pull:`11121`). + By `Nick Hodgskin `_. - Add stubtest configuration and allowlist for validating type annotations against runtime behavior. This enables CI integration for type stub validation and helps diff --git a/pixi.toml b/pixi.toml index 2d9affbb2d4..5a3094a95ed 100644 --- a/pixi.toml +++ b/pixi.toml @@ -346,6 +346,12 @@ rst-to-myst = { version = "*", extras = ["sphinx"] } [feature.tools.tasks] rst2myst = { cmd = "rst2myst", description = "Convert rst to myst markdown" } +[feature.numpydoc-lint.dependencies] +numpydoc = "*" + +[feature.numpydoc-lint.tasks] +numpydoc-lint = { cmd = "python ci/numpydoc-public-api.py" } + [environments] # Testing # test-just-xarray = { features = ["test"] } # https://github.com/pydata/xarray/pull/10888/files#r2511336147 @@ -458,4 +464,5 @@ default = { features = [ "dev", ] } policy = { features = ["policy"], no-default-feature = true } +numpydoc-lint = { features = ["numpydoc-lint"] } tools = { features = ["tools"], no-default-feature = true } From ba7ab33cd74271647927d5f06896d2112ce3ba7d Mon Sep 17 00:00:00 2001 From: Nick Hodgskin <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:36:33 +0200 Subject: [PATCH 2/2] DOC: Update communication channels (#11254) --- CORE_TEAM_GUIDE.md | 2 +- doc/contribute/contributing.rst | 7 ++++--- doc/get-help/help-diagram.rst | 4 ++-- doc/get-help/socials.rst | 1 - doc/whats-new.rst | 2 ++ 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CORE_TEAM_GUIDE.md b/CORE_TEAM_GUIDE.md index 93c7b2092fe..f111b735025 100644 --- a/CORE_TEAM_GUIDE.md +++ b/CORE_TEAM_GUIDE.md @@ -274,7 +274,7 @@ resources such as: - [python-xarray](https://stackoverflow.com/questions/tagged/python-xarray) on Stack Overflow. - The Xarray channel in the [OSSci Zulip](https://ossci.zulipchat.com/) - [@xarray_dev](https://x.com/xarray_dev) on X. -- [xarray-dev](https://discord.gg/bsSGdwBn) discord community (normally only used for remote synchronous chat during sprints). +- **(superseded by the OSSci Zulip)** [xarray-dev](https://discord.com/invite/wEKPCt4PDu) discord community (normally only used for remote synchronous chat during sprints). You are not required to monitor any of the social resources. diff --git a/doc/contribute/contributing.rst b/doc/contribute/contributing.rst index 654f8e3cc8a..4b1dd12af2e 100644 --- a/doc/contribute/contributing.rst +++ b/doc/contribute/contributing.rst @@ -19,9 +19,10 @@ and other ideas are welcome. If you have any questions on the process or how to fix something feel free to ask us! The recommended places to ask questions are `GitHub Discussions `_ -or the Xarray channel in the `OSSci Zulip `_. We also have a `Discord `_ and a -`mailing list `_. There is also a -`"python-xarray" tag on Stack Overflow `_ which we monitor for questions. +or the Xarray channel in the `OSSci Zulip `_. + +In the past, we had a `Discord `_ and a +`mailing list `_. Feel free to browse historical conversations there. We also have a biweekly community call, details of which are announced on the `Developers meeting `_. diff --git a/doc/get-help/help-diagram.rst b/doc/get-help/help-diagram.rst index 87fbc0edbb4..0e9f0efea02 100644 --- a/doc/get-help/help-diagram.rst +++ b/doc/get-help/help-diagram.rst @@ -47,7 +47,7 @@ Help Flowchart fab:fa-readme Dask fab:fa-readme Rioxarray]):::ansNodefmt help([fab:fa-github Xarray Discussions - fab:fa-discord Xarray Discord + fa:fa-globe OSSci Zulip fa:fa-globe Pangeo Discourse]):::ansNodefmt bug([Let us know: fab:fa-github Xarray Issues]):::ansNodefmt @@ -88,7 +88,7 @@ Flowchart links - `Xarray Docs `__ - `Stack Exchange `__ - `Xarray Discussions `__ -- `Xarray Discord `__ +- `OSSci Zulip `__ - `Xarray Office Hours `__ - `Pangeo Discourse `__ - `Xarray Issues `__ diff --git a/doc/get-help/socials.rst b/doc/get-help/socials.rst index b63b3529a73..96655cc0557 100644 --- a/doc/get-help/socials.rst +++ b/doc/get-help/socials.rst @@ -5,6 +5,5 @@ Social Media Xarray is active on several social media platforms. We use these platforms to share updates and connect with the user community. -- `Discord `__ - `Bluesky `__ - `Twitter(X) `__ diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 6c7680f6619..81d901068b3 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -120,6 +120,8 @@ Bug Fixes Documentation ~~~~~~~~~~~~~ +- Update documentation and team guide to promote Zulip. Remove mentions of Discord (:pull:`11246`, :pull:`11254`). + By `Nick Hodgskin `_. - Fix typos (:pull:`11180`, :pull:`11181`, :pull:`11182`, :pull:`11185`, :pull:`11186`). By `Yaocheng Chen `_. - Fix code blocks on "how to create custom index" doc page (:pull:`11255`).