From 084baf6c830a00f2714545ff6a68b5e445e6cf8d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 25 Sep 2026 11:23:52 -0400 Subject: [PATCH] Minor fixes to spacing check --- .circleci/config.yml | 46 +++++++++++++------------------- RELEASE.rst | 5 ++-- numpydoc/docscrape.py | 11 +++++++- numpydoc/tests/test_docscrape.py | 41 ++++++++++++++++++++++++++++ tools/integration/build_docs.py | 19 +++++++++++++ 5 files changed, 90 insertions(+), 32 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f22cc14d..fcd28cd2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,18 +1,22 @@ version: 2.1 # gen3 resource classes are only valid with the `current` machine image tag -- a -# pinned dated tag is rejected with "is not a valid resource class". -_machine: &machine - machine: - image: ubuntu-2604:current - # large.gen3: 4 vCPUs / 16 GiB, both faster and cheaper per minute than the - # gen1 `large` class. Revert to `large` if gen3 queue times become a problem. - resource_class: large.gen3 +# pinned dated tag is rejected with "is not a valid resource class". They are +# both faster and cheaper per minute than their gen1 equivalents; revert to +# `medium` / `large` if gen3 queue times become a problem. +executors: + linux: + parameters: + size: + type: string + default: medium.gen3 + machine: + image: ubuntu-2604:current + resource_class: << parameters.size >> jobs: build_docs: - docker: - - image: "cimg/python:3.11" + executor: linux steps: - checkout - run: @@ -20,7 +24,7 @@ jobs: command: sudo apt-get update - run: name: Install TeX - command: sudo apt install dvipng texlive-fonts-recommended texlive-latex-recommended texlive-latex-extra latexmk texlive-xetex + command: sudo apt-get install -yy --no-install-recommends dvipng texlive-fonts-recommended texlive-latex-recommended texlive-latex-extra latexmk texlive-xetex - restore_cache: keys: - pip-cache @@ -71,26 +75,12 @@ jobs: description: Debian packages the project's conf.py needs on PATH. type: string default: "" - <<: *machine + # large.gen3: 4 vCPUs / 16 GiB + executor: + name: linux + size: large.gen3 steps: - checkout - # Pull requests build only when they carry the `integration-tests` label; - # pushes to main always do. CircleCI does not retrigger on label events, - # so a label added later needs a re-run of the workflow. A GitHub API - # hiccup (the CircleCI IP is shared and rate-limited) builds anyway. - - run: - name: Check for the integration-tests label - command: | - [ -n "$CIRCLE_PULL_REQUEST" ] || exit 0 - API=$(echo "$CIRCLE_PULL_REQUEST" | - sed -E 's#.*github.com/(.+)/pull/([0-9]+)#https://api.github.com/repos/\1/pulls/\2#') - LABELS=$(curl -fsS "$API" | python3 -c \ - 'import json,sys; print("\n".join(l["name"] for l in json.load(sys.stdin)["labels"]))' \ - 2>/dev/null) || { - echo "Could not read labels from $API; building anyway."; exit 0; } - echo "$LABELS" | grep -qx integration-tests || { - echo "Not labelled integration-tests; skipping $CIRCLE_JOB." - circleci-agent step halt; } - run: name: Install apt packages command: | diff --git a/RELEASE.rst b/RELEASE.rst index 2f1086c8..735f7f72 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -13,9 +13,8 @@ version string to edit by hand. Example ``__version__`` values: - 1.9.dev2+gdef # 2 commits after ``v1.8`` (development version) The CircleCI ``integration`` jobs build numpy, matplotlib, scikit-image and -networkx against this checkout; they run on every push to ``main`` and on a -pull request only once it carries the ``integration-tests`` label (add the -label, then re-run the workflow). ``scipy`` is not in CI -- run +networkx against this checkout on every push, including to pull requests. +``scipy`` is not in CI -- run ``python tools/integration/build_docs.py scipy`` by hand before a release. Process diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index c134d932..39d5e626 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -220,6 +220,9 @@ def _read_sections(self): else: yield name, self._strip(data[2:]) + # "name: type" or "x1, x2: type", optionally with leading * or ** + _missing_colon_space_rgx = re.compile(r"^\*{0,2}\w+(\s*,\s*\*{0,2}\w+)*: ") + def _parse_param_list(self, content, single_element_is_type=False): content = dedent_lines(content) @@ -235,7 +238,13 @@ def _parse_param_list(self, content, single_element_is_type=False): # much. So, we compact any run of 2+ whitespace. arg_type = re.sub(r"\s{2,}", " ", arg_type_w_whitespace) else: - if not single_element_is_type and ": " in header: + # Only warn when what precedes ": " looks like parameter + # name(s), so that prose like ".. note:: ..." or + # "Implementation note: ..." that ended up in a parameter + # section does not trigger a misleading warning. + if not single_element_is_type and self._missing_colon_space_rgx.match( + header + ): self._error_location( f"Parameter {header!r} has no space before the colon", error=False, diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index c438dc42..1d735912 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -219,6 +219,47 @@ def test_parameter_missing_space_before_colon_warns(): assert params == [("val: int", "", ["Input value."])] +@pytest.mark.parametrize( + "header", + [ + "x1, x2: array_like", + "*args: tuple", + "**kwargs: dict, optional", + "default: any Python object (default=None)", + ], +) +def test_parameter_missing_space_before_colon_warns_variants(header): + doc = f""" + Parameters + ---------- + {header} + Input value. + """ + with pytest.warns(UserWarning, match="space before the colon"): + NumpyDocString(doc) + + +@pytest.mark.parametrize( + "header", + [ + ".. note:: You must specify exactly one of deg or rad.", + "Implementation note: this function creates an intermediate graph", + ], +) +def test_parameter_prose_with_colon_does_not_warn(header): + doc = f""" + Parameters + ---------- + val : int + Input value. + + {header} + """ + with warnings.catch_warnings(): + warnings.simplefilter("error") + NumpyDocString(doc) + + def test_type_continuation(): doc = NumpyDocString(""" Parameters diff --git a/tools/integration/build_docs.py b/tools/integration/build_docs.py index a81f52a0..27553a5d 100644 --- a/tools/integration/build_docs.py +++ b/tools/integration/build_docs.py @@ -85,6 +85,17 @@ class Project: exclude_requirements=("pygraphviz",), # Don't execute the gallery examples or the myst-nb tutorial. sphinx_defines=("plot_gallery=0", "nb_execution_mode=off"), + # The networkx 3.7 release notes have an unbalanced `*Matcher` that + # docutils warns about (fixed upstream after 3.7); a no-op otherwise. + pre_build_cwd="doc", + pre_build=( + ( + "sed", + "-i", + r"s/^- Expose \*Matcher classes/- Expose ``*Matcher`` classes/", + "release/release_3.7.rst", + ), + ), ), # Upstream builds with SPHINXOPTS="-W -j auto" and so do we, once the images # `plot_gallery=0` never renders are suppressed. suppress_warnings repeats @@ -129,6 +140,14 @@ class Project: # them instead would defeat the point. pre_build_cwd="doc", pre_build=(("sed", "-i", "-E", _MPL_SED, "missing-references.json"),), + # conf.py turns every warning into an error, and released matplotlib + # has "name: type" parameter headers that numpydoc now warns about + # (fixed upstream after 3.11.x). Keep them visible but non-fatal. + conf_append="""\ +warnings.filterwarnings( + "default", message=r".*has no space before the colon", category=UserWarning +) +""", ), # numpy and scipy get sphinx-build pointed straight at doc/source: their # Makefiles insist the installed library was built from the checkout.