From 20572d381bf5a86dd81452cf4f98d2dfa8b658be Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:22:49 -0500 Subject: [PATCH 01/21] chore(uv[cooldown]): Exempt ruff from release cooldown why: uv's global `exclude-newer = "3 days"` supply-chain cooldown hides ruff 0.16.0 (released 2026-07-23) from the resolver, so the version floor in the next commit cannot resolve. what: - Add `ruff = false` to `[tool.uv.exclude-newer-package]` Temporary. Revert before merge - the cooldown clears on its own and the `ruff>=0.16` floor is what actually holds the version. --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4a9541fc..7ae7e9d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,10 @@ sphinx-vite-builder = { workspace = true } [tool.uv.exclude-newer-package] # Adopt gp-libs releases immediately, bypassing the global cooldown. gp-libs = false +# TEMPORARY - REVERT BEFORE MERGE. ruff 0.16.0 released 2026-07-23 and is +# still inside the 3-day cooldown, so the resolver cannot see it. Drop this +# line once the cooldown clears; the version floor is what holds ruff 0.16. +ruff = false [dependency-groups] dev = [ From 4a4fe337b497cc19bf05ba5a82a3609945ad8684 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:23:06 -0500 Subject: [PATCH 02/21] py(deps[dev]): Require ruff>=0.16.0 why: ruff 0.16.0 stabilizes rules inside prefixes this workspace already selects and starts formatting Python code blocks in Markdown. Pinning a floor keeps contributors and CI on the same diagnostics instead of splitting on whatever ruff each machine resolved. what: - Raise `ruff` to `>=0.16.0` in the `dev` dependency group - Relock `uv.lock`: ruff 0.15.22 -> 0.16.0 https://astral.sh/blog/ruff-v0.16.0 --- pyproject.toml | 2 +- uv.lock | 45 +++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7ae7e9d0..24a72a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dev = [ "coverage", "pytest-cov", # Lint - "ruff", + "ruff>=0.16.0", "mypy", "tomli; python_version < '3.11'", "typing-extensions", diff --git a/uv.lock b/uv.lock index 05b25916..53069b87 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ exclude-newer-span = "P3D" [options.exclude-newer-package] gp-libs = false +ruff = false [manifest] members = [ @@ -553,7 +554,7 @@ dev = [ { name = "pytest-playwright" }, { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "sphinx-autobuild" }, { name = "sphinx-autodoc-api-style", editable = "packages/sphinx-autodoc-api-style" }, { name = "sphinx-autodoc-argparse", editable = "packages/sphinx-autodoc-argparse" }, @@ -1460,27 +1461,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, - { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, - { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, - { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] From e2b0727eacb88dd60842b7bf7dad816dd8e75a1d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:26:22 -0500 Subject: [PATCH 03/21] tests(style[scenarios]): Sort None last in unions why: ruff 0.16.0 stabilizes RUF036, which requires `None` to be the last member of a type union. It reads as the fallback case and matches how `Optional[X]` and typeshed order their unions. what: - Move `None` to the end of the `ScenarioInputValue` and `FrozenScenarioValue` type aliases https://docs.astral.sh/ruff/rules/none-not-at-end-of-union/ --- tests/_sphinx_scenarios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_sphinx_scenarios.py b/tests/_sphinx_scenarios.py index 035a7fea..51ac84e9 100644 --- a/tests/_sphinx_scenarios.py +++ b/tests/_sphinx_scenarios.py @@ -65,21 +65,21 @@ | int | float | bool - | None | list["ScenarioInputValue"] | tuple["ScenarioInputValue", ...] | set["ScenarioInputValue"] | frozenset["ScenarioInputValue"] | dict[str, "ScenarioInputValue"] + | None ) FrozenScenarioValue: t.TypeAlias = ( str | int | float | bool - | None | tuple["FrozenScenarioValue", ...] | dict[str, "FrozenScenarioValue"] + | None ) From a2ef4e8fa16352cb7a760abd089da98f655134a9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:26:40 -0500 Subject: [PATCH 04/21] docs(style[md]): Format Python blocks in Markdown why: ruff 0.16.0 formats Python and pycon code blocks inside Markdown by default, so `ruff format . --check` now fails CI on READMEs and docs pages that were previously out of scope. The churn is mechanical - the formatter applying its existing Python rules to embedded snippets. what: - Reformat Python code blocks in the workspace and package READMEs and in the docs pages, matching `ruff format` output https://astral.sh/blog/ruff-v0.16.0 --- README.md | 1 + docs/packages/sphinx-vite-builder/how-to.md | 2 +- docs/quickstart.md | 2 ++ packages/gp-sphinx/README.md | 1 + packages/sphinx-autodoc-fastmcp/README.md | 6 +++++- packages/sphinx-ux-badges/README.md | 16 +++++++++------- packages/sphinx-vite-builder/README.md | 2 +- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 51a3d73f..b3cb992e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Replace your `docs/conf.py` with: ```python """Sphinx configuration for my-project.""" + from __future__ import annotations from gp_sphinx.config import merge_sphinx_config diff --git a/docs/packages/sphinx-vite-builder/how-to.md b/docs/packages/sphinx-vite-builder/how-to.md index cd3d3d86..2272e1ed 100644 --- a/docs/packages/sphinx-vite-builder/how-to.md +++ b/docs/packages/sphinx-vite-builder/how-to.md @@ -33,7 +33,7 @@ SIGKILL teardown on signal / `atexit`. ```python # docs/conf.py extensions = ["sphinx_vite_builder"] -sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod" +sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod" sphinx_vite_builder_root = "/abs/path/to/web" ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 6bea04ba..3774d9ae 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -62,6 +62,7 @@ In your project's `docs/conf.py`: ```python """Sphinx configuration for my-project.""" + from __future__ import annotations from gp_sphinx.config import merge_sphinx_config @@ -145,6 +146,7 @@ Create a file `my_module.py` next to your `docs/` directory: ```python """Demo module for the autodoc design system.""" + from __future__ import annotations from typing import Any diff --git a/packages/gp-sphinx/README.md b/packages/gp-sphinx/README.md index 00a31de4..ff05fe85 100644 --- a/packages/gp-sphinx/README.md +++ b/packages/gp-sphinx/README.md @@ -20,6 +20,7 @@ Replace ~300 lines of duplicated `docs/conf.py` with ~10 lines: ```python """Sphinx configuration for my-project.""" + from __future__ import annotations from gp_sphinx.config import merge_sphinx_config diff --git a/packages/sphinx-autodoc-fastmcp/README.md b/packages/sphinx-autodoc-fastmcp/README.md index 0faf0acf..f78c0f75 100644 --- a/packages/sphinx-autodoc-fastmcp/README.md +++ b/packages/sphinx-autodoc-fastmcp/README.md @@ -33,7 +33,11 @@ fastmcp_area_map = { } fastmcp_model_module = "myproject.models" fastmcp_model_classes = {"SessionInfo", "WindowInfo"} -fastmcp_section_badge_map = {"Inspect": "readonly", "Act": "mutating", "Destroy": "destructive"} +fastmcp_section_badge_map = { + "Inspect": "readonly", + "Act": "mutating", + "Destroy": "destructive", +} fastmcp_section_badge_pages = {"tools/index", "index"} fastmcp_collector_mode = "register" # or "introspect" ``` diff --git a/packages/sphinx-ux-badges/README.md b/packages/sphinx-ux-badges/README.md index e2596825..556ce8f6 100644 --- a/packages/sphinx-ux-badges/README.md +++ b/packages/sphinx-ux-badges/README.md @@ -27,13 +27,15 @@ Then build badges in your directives or transforms: ```python from sphinx_ux_badges import build_badge, build_badge_group, build_toolbar -group = build_badge_group([ - build_badge( - "readonly", - tooltip="Read-only", - classes=["gp-sphinx-fastmcp__safety-readonly"], - ), -]) +group = build_badge_group( + [ + build_badge( + "readonly", + tooltip="Read-only", + classes=["gp-sphinx-fastmcp__safety-readonly"], + ), + ] +) ``` ## Documentation diff --git a/packages/sphinx-vite-builder/README.md b/packages/sphinx-vite-builder/README.md index 59879755..9f45f212 100644 --- a/packages/sphinx-vite-builder/README.md +++ b/packages/sphinx-vite-builder/README.md @@ -156,7 +156,7 @@ during `sphinx-build` (one-shot) and `sphinx-autobuild` (watched). ```python # docs/conf.py extensions = ["sphinx_vite_builder"] -sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod" +sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod" sphinx_vite_builder_root = "/abs/path/to/web" ``` From 08af70e1d27910b827d9cf0e0a64a061d10a6ca9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:31:33 -0500 Subject: [PATCH 05/21] docs(CHANGES): Note ruff 0.16 lint toolchain bump why: Contributors formatting with an older ruff will revert the Markdown code blocks this bump reflowed, so the floor belongs in the changelog. what: - Add a `### Development` entry for the `ruff>=0.16.0` floor and the new Markdown code-block formatting --- CHANGES | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGES b/CHANGES index e3ea8974..9552aeed 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,19 @@ $ uv add gp-sphinx --prerelease allow +### Development + +#### Lint toolchain: ruff 0.16 + +Minimum `ruff>=0.16.0` (was unpinned), so contributors and CI agree on +which diagnostics fire. See the +[release announcement](https://astral.sh/blog/ruff-v0.16.0). + +`ruff format` now formats Python code blocks embedded in Markdown, which +reflows snippets in the READMEs and docs pages. Contributors who run the +formatter on an older ruff will see those blocks revert, so upgrade +before committing. (#73) + ## gp-sphinx 0.0.1a36 (2026-07-26) gp-sphinx 0.0.1a36 makes a class that documents its fields in an From cafac6cab76639738fbfaf34a6312421c27e7127 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 14:22:43 -0500 Subject: [PATCH 06/21] Revert "chore(uv[cooldown]): Exempt ruff from release cooldown" why: ruff 0.16.0 published 2026-07-23T19:10Z and has now cleared uv's 3-day supply-chain cooldown, so the resolver reaches it unaided. The `ruff>=0.16.0` floor is what holds the version; leaving the exemption would permanently opt ruff out of the cooldown guard. what: - Drop `ruff = false` from `[tool.uv.exclude-newer-package]` - Relock: the setting is recorded in `uv.lock`, so removing it forces a re-resolve. ruff stays at 0.16.0 and no other package moves. This reverts commit 20572d381bf5a86dd81452cf4f98d2dfa8b658be. --- pyproject.toml | 4 ---- uv.lock | 1 - 2 files changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24a72a6a..7e969d48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,10 +40,6 @@ sphinx-vite-builder = { workspace = true } [tool.uv.exclude-newer-package] # Adopt gp-libs releases immediately, bypassing the global cooldown. gp-libs = false -# TEMPORARY - REVERT BEFORE MERGE. ruff 0.16.0 released 2026-07-23 and is -# still inside the 3-day cooldown, so the resolver cannot see it. Drop this -# line once the cooldown clears; the version floor is what holds ruff 0.16. -ruff = false [dependency-groups] dev = [ diff --git a/uv.lock b/uv.lock index 53069b87..fb997347 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,6 @@ exclude-newer-span = "P3D" [options.exclude-newer-package] gp-libs = false -ruff = false [manifest] members = [ From de6a1f0946ca28658d13d8f14a847aa6b89280d2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:04:36 -0500 Subject: [PATCH 07/21] chore(ruff[lint]): Adopt ruff's default rule set why: ruff 0.16 ships a curated default rule set as its recommended baseline. An explicit `select` replaces that set rather than extending it, so this workspace was running 351 rules and silently opting out of the rest. `extend-select` layers the project's own linters on top of the default set instead of in place of it. what: - Replace `select` with `extend-select`, same entries, one per line - Note in the file why `select` stays unset Enabled rules go from 351 to 565. https://docs.astral.sh/ruff/linter/#rule-selection --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7e969d48..7ad4289c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,10 @@ extend-exclude = [ ] [tool.ruff.lint] -select = [ +# `select` is deliberately unset: ruff 0.16 enables a curated default rule +# set, and an explicit `select` would replace it rather than extend it. +# `extend-select` layers this project's additional linters on top. +extend-select = [ "E", # pycodestyle "F", # pyflakes "I", # isort @@ -158,7 +161,7 @@ select = [ "PERF", # Perflint "RUF", # Ruff-specific rules "D", # pydocstyle - "FA100", # future annotations + "FA100", # future annotations ] ignore = [ "COM812", # missing trailing comma, ruff format conflict From 815a7d956fa34742e353a3acf8f27145298bc2b0 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:07:19 -0500 Subject: [PATCH 08/21] docs(ext[package-reference]): Strip affixes without slicing why: `url[len("git+") :]` and `url[: -len(".git")]` restate the affix twice each and depend on the slice bounds staying in sync with the literal. `str.removeprefix` / `str.removesuffix` say the same thing once and carry the "only when present" guard themselves. what: - Replace the conditional slices with `removeprefix` / `removesuffix` https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/ --- docs/_ext/package_reference.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/_ext/package_reference.py b/docs/_ext/package_reference.py index 024b5988..dde58ef4 100644 --- a/docs/_ext/package_reference.py +++ b/docs/_ext/package_reference.py @@ -458,10 +458,8 @@ def _repository_url_from_package_json(manifest: dict[str, t.Any]) -> str: return repo if isinstance(repo, dict): url = str(repo.get("url", "")) - if url.startswith("git+"): - url = url[len("git+") :] - if url.endswith(".git"): - url = url[: -len(".git")] + url = url.removeprefix("git+") + url = url.removesuffix(".git") return url return "" From f72a6ebd41106d08ea9157b2c2ff0adabce2df27 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:07:31 -0500 Subject: [PATCH 09/21] test(badges[palettes]): Spell the regex dotall flag out why: `re.S` is a one-letter alias whose meaning has to be looked up. `re.DOTALL` names what it does at the call site, and the palette patterns rely on that flag to span CSS blocks across newlines. what: - Use `re.DOTALL` instead of `re.S` in the three block patterns https://docs.astral.sh/ruff/rules/regex-flag-alias/ --- tests/ext/badges/test_palettes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ext/badges/test_palettes.py b/tests/ext/badges/test_palettes.py index 657e6709..981a827c 100644 --- a/tests/ext/badges/test_palettes.py +++ b/tests/ext/badges/test_palettes.py @@ -26,12 +26,12 @@ _VAR_REFERENCE = re.compile(r"var\((--gp-sphinx-badge-[\w-]+)\)") _TOKEN_SUFFIX = re.compile(r"-(bg|fg|border)$") -_ROOT_BLOCK = re.compile(r":root \{(.*?)\n\}", re.S) +_ROOT_BLOCK = re.compile(r":root \{(.*?)\n\}", re.DOTALL) _MEDIA_DARK_BLOCK = re.compile( r"@media \(prefers-color-scheme: dark\) \{(.*?)\n\}\n", - re.S, + re.DOTALL, ) -_BODY_DARK_BLOCK = re.compile(r'\nbody\[data-theme="dark"\] \{(.*?)\n\}', re.S) +_BODY_DARK_BLOCK = re.compile(r'\nbody\[data-theme="dark"\] \{(.*?)\n\}', re.DOTALL) def _palette_css() -> str: From 0d6033c175ebff22aa1fd9c135724eb0b8a763e4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:02 -0500 Subject: [PATCH 10/21] style(lint[tc005]): Drop empty TYPE_CHECKING blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Two modules kept an `if t.TYPE_CHECKING:` guard whose body was a bare `pass` — scaffolding left behind after the last type-only import moved out. It reads as though something is imported under it. what: - Delete the empty guards in `_internal/process.py` and `tests/test_gp_furo_theme.py` - Drop the now-unused `typing` import the guard was the last user of https://docs.astral.sh/ruff/rules/empty-type-checking-block/ --- .../src/sphinx_vite_builder/_internal/process.py | 3 --- tests/test_gp_furo_theme.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/process.py b/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/process.py index d3fb7418..40f05706 100644 --- a/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/process.py +++ b/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/process.py @@ -38,9 +38,6 @@ import sys import typing as t -if t.TYPE_CHECKING: - pass - _module_logger = logging.getLogger(__name__) diff --git a/tests/test_gp_furo_theme.py b/tests/test_gp_furo_theme.py index 309521ef..27ed61ad 100644 --- a/tests/test_gp_furo_theme.py +++ b/tests/test_gp_furo_theme.py @@ -12,7 +12,6 @@ import importlib.metadata import pathlib import textwrap -import typing as t import pytest from gp_furo_theme import THEME_NAME, get_theme_path, get_vite_root, setup @@ -25,9 +24,6 @@ read_output, ) -if t.TYPE_CHECKING: - pass - def test_theme_path_exists() -> None: """Theme directory is bundled in the package.""" From 3f7d8d75b4335a89170829a99948143918582968 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:28 -0500 Subject: [PATCH 11/21] pkg(pytest-fixtures[transforms]): Drop stray pass placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The `if t.TYPE_CHECKING:` block already imports two names, so the trailing `pass` is dead — it reads like the block is empty when it is not. what: - Remove the `pass` from the TYPE_CHECKING block https://docs.astral.sh/ruff/rules/unnecessary-placeholder/ --- .../src/sphinx_autodoc_pytest_fixtures/_transforms.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_transforms.py b/packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_transforms.py index 5cd74d69..21b597ff 100644 --- a/packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_transforms.py +++ b/packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_transforms.py @@ -22,8 +22,6 @@ from sphinx.application import Sphinx from sphinx.domains.python import PythonDomain - pass - logger = sphinx_logging.getLogger(__name__) _PARAMETER_FIELD_LABELS = frozenset( From 90240979742ba29de82397583129fa32b98d8ed1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:48 -0500 Subject: [PATCH 12/21] style(lint[pie810]): Test prefixes with one startswith call why: Chained `x.startswith(a) or x.startswith(b)` walks the string twice and repeats the receiver. `str.startswith` accepts a tuple and short-circuits internally, so the tuple form is both shorter and the one CPython optimises. what: - Collapse the paired `startswith` calls in the em-dash separator check and the how-to prose policy test https://docs.astral.sh/ruff/rules/multiple-starts-ends-with/ --- .../src/sphinx_autodoc_typehints_gp/_field_xref_transform.py | 2 +- tests/docs/test_docs_policy.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/_field_xref_transform.py b/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/_field_xref_transform.py index 3bfdaaf8..f94b04c0 100644 --- a/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/_field_xref_transform.py +++ b/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/_field_xref_transform.py @@ -274,7 +274,7 @@ def _is_em_dash_separator(text: str) -> bool: False """ stripped = text.lstrip() - return stripped.startswith(_EN_DASH + " ") or stripped.startswith("-- ") + return stripped.startswith((_EN_DASH + " ", "-- ")) _PROSE_FIELD_TOKENS: t.Final = ( diff --git a/tests/docs/test_docs_policy.py b/tests/docs/test_docs_policy.py index 9ca8d9fb..08cab406 100644 --- a/tests/docs/test_docs_policy.py +++ b/tests/docs/test_docs_policy.py @@ -187,9 +187,9 @@ def test_package_how_to_pages_open_with_concept_prose() -> None: for path in sorted((DOCS_ROOT / "packages").glob("*/how-to.md")): for line in path.read_text(encoding="utf-8").splitlines(): stripped = line.strip() - if not stripped or stripped.startswith("(") or stripped.startswith("#"): + if not stripped or stripped.startswith(("(", "#")): continue - if stripped.startswith("```") or stripped.startswith("::::"): + if stripped.startswith(("```", "::::")): offenders.append(str(path.relative_to(REPO_ROOT))) break From 67c255b961613fa1c71a74b274a86c8e484a3648 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:24 -0500 Subject: [PATCH 13/21] pkg(vite-builder[vite]): Delete dead private type aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: `_AsyncProcess` and `_AsyncioBus` are private, absent from `__all__`, and referenced nowhere — the comment above them claims they are re-exports for consumers, but a leading underscore makes them unimportable in that sense. The module already uses `AsyncProcess` and `AsyncioBus` directly. what: - Remove both aliases and the comment describing them - Drop the `typing` import they were the last user of https://docs.astral.sh/ruff/rules/unused-private-type-alias/ --- .../src/sphinx_vite_builder/_internal/vite.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/vite.py b/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/vite.py index 60580f5a..6b3bcc2a 100644 --- a/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/vite.py +++ b/packages/sphinx-vite-builder/src/sphinx_vite_builder/_internal/vite.py @@ -22,7 +22,6 @@ import pathlib import shutil import textwrap -import typing as t from .bus import AsyncioBus from .errors import ( @@ -395,9 +394,3 @@ def run_vite_build( "vite_build_command", "vite_watch_command", ) - - -# Re-exports for type-checker friendliness when consumers import -# the orchestration module directly. -_AsyncProcess: t.TypeAlias = AsyncProcess -_AsyncioBus: t.TypeAlias = AsyncioBus From a4453fce13c3f87812f152dbc7fe3b05cc1b687c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:45 -0500 Subject: [PATCH 14/21] style(lint[pyi025]): Alias collections.abc.Set on import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: `from collections.abc import Set` shadows nothing but reads as the `set` builtin at every annotation site, and these are exactly the signatures where the distinction matters — Sphinx hands `merge_domaindata` and `prepare_writing` an immutable set view, not a `set`. what: - Import `Set as AbstractSet` and annotate with `AbstractSet` in the three domains, the demo builder, and the OpenGraph description parser https://docs.astral.sh/ruff/rules/unaliased-collections-abc-set-import/ --- docs/_ext/sphinx_demo_builder.py | 4 ++-- .../src/sphinx_autodoc_argparse/domain.py | 4 ++-- .../src/sphinx_autodoc_docutils/domain.py | 4 ++-- .../src/sphinx_autodoc_sphinx/domain.py | 4 ++-- .../src/sphinx_gp_opengraph/_description.py | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/_ext/sphinx_demo_builder.py b/docs/_ext/sphinx_demo_builder.py index 9d412765..25d1b53c 100644 --- a/docs/_ext/sphinx_demo_builder.py +++ b/docs/_ext/sphinx_demo_builder.py @@ -21,7 +21,7 @@ from sphinx.roles import XRefRole if t.TYPE_CHECKING: - from collections.abc import Iterator, Set + from collections.abc import Iterator, Set as AbstractSet from docutils import nodes from sphinx.application import Sphinx @@ -49,7 +49,7 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str: """Return the in-archive URI for a document.""" return f"{docname}.txt" - def prepare_writing(self, docnames: Set[str]) -> None: + def prepare_writing(self, docnames: AbstractSet[str]) -> None: """No writer state is needed for the demo.""" def write_doc(self, docname: str, doctree: nodes.document) -> None: diff --git a/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py b/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py index a5bc02d9..86768e96 100644 --- a/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py +++ b/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py @@ -34,7 +34,7 @@ from sphinx.util.nodes import make_refnode if t.TYPE_CHECKING: - from collections.abc import Iterable, Iterator, Set + from collections.abc import Iterable, Iterator, Set as AbstractSet from docutils import nodes from docutils.nodes import Element @@ -299,7 +299,7 @@ def clear_doc(self, docname: str) -> None: def merge_domaindata( self, - docnames: Set[str], + docnames: AbstractSet[str], otherdata: dict[str, t.Any], ) -> None: """Merge sibling worker's ``domaindata`` under parallel builds.""" diff --git a/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py b/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py index 5e94526e..449adfda 100644 --- a/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py +++ b/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py @@ -36,7 +36,7 @@ from sphinx.util.nodes import make_id, make_refnode if t.TYPE_CHECKING: - from collections.abc import Iterable, Iterator, Set + from collections.abc import Iterable, Iterator, Set as AbstractSet from docutils import nodes from docutils.nodes import Element @@ -303,7 +303,7 @@ def clear_doc(self, docname: str) -> None: def merge_domaindata( self, - docnames: Set[str], + docnames: AbstractSet[str], otherdata: dict[str, t.Any], ) -> None: """Merge sibling worker's ``domaindata`` under parallel builds.""" diff --git a/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py b/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py index 7a3f605f..5d7a816d 100644 --- a/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py +++ b/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py @@ -36,7 +36,7 @@ from sphinx.util.nodes import make_id, make_refnode if t.TYPE_CHECKING: - from collections.abc import Iterable, Iterator, Set + from collections.abc import Iterable, Iterator, Set as AbstractSet from docutils import nodes from docutils.nodes import Element @@ -268,7 +268,7 @@ def clear_doc(self, docname: str) -> None: def merge_domaindata( self, - docnames: Set[str], + docnames: AbstractSet[str], otherdata: dict[str, t.Any], ) -> None: """Merge sibling worker's ``domaindata`` under parallel builds.""" diff --git a/packages/sphinx-gp-opengraph/src/sphinx_gp_opengraph/_description.py b/packages/sphinx-gp-opengraph/src/sphinx_gp_opengraph/_description.py index 08dbe3d9..03e7331d 100644 --- a/packages/sphinx-gp-opengraph/src/sphinx_gp_opengraph/_description.py +++ b/packages/sphinx-gp-opengraph/src/sphinx_gp_opengraph/_description.py @@ -26,13 +26,13 @@ from docutils import nodes if t.TYPE_CHECKING: - from collections.abc import Set + from collections.abc import Set as AbstractSet def get_description( doctree: nodes.document, description_length: int, - known_titles: Set[str] = frozenset(), + known_titles: AbstractSet[str] = frozenset(), ) -> str: """Return a plain-text description extracted from ``doctree``. @@ -83,7 +83,7 @@ def __init__( document: nodes.document, *, desc_len: int, - known_titles: Set[str] = frozenset(), + known_titles: AbstractSet[str] = frozenset(), ) -> None: super().__init__(document) self.description = "" From 309e9d2205455a9acd8cf0d2210e2818a50a7f43 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:10:17 -0500 Subject: [PATCH 15/21] style(lint[isc004]): Parenthesize concatenated collection items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: A multi-line implicit concatenation sitting bare inside a list or tuple literal is indistinguishable from a missing comma — the reader has to count elements to tell whether two sentences are one item or two. Parentheses make the intent explicit at the site. what: - Wrap the concatenated cluster blurbs, generated-page lead paragraphs, and the expected warning format string in parentheses https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/ --- docs/_ext/package_reference.py | 50 ++++++++++++------- .../test_sphinx_pytest_fixtures.py | 6 ++- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/docs/_ext/package_reference.py b/docs/_ext/package_reference.py index dde58ef4..b792ca19 100644 --- a/docs/_ext/package_reference.py +++ b/docs/_ext/package_reference.py @@ -935,14 +935,18 @@ def maturity_badge(maturity: str) -> str: ( "autodoc", "Autodoc extensions", - "Domain-specific autodoc extensions: each adds directives that " - "generate documentation from a particular source-construct family.", + ( + "Domain-specific autodoc extensions: each adds directives that " + "generate documentation from a particular source-construct family." + ), ), ( "ux", "UX components", - "Badge primitives, layout presenters, and other shared " - "rendering helpers consumed by the autodoc family.", + ( + "Badge primitives, layout presenters, and other shared " + "rendering helpers consumed by the autodoc family." + ), ), ( "highlighting", @@ -952,8 +956,10 @@ def maturity_badge(maturity: str) -> str: ( "build-seo", "Build & SEO", - "PEP 517 backends, build orchestration, and crawl-indexing " - "extensions auto-loaded by gp-sphinx when ``docs_url`` is set.", + ( + "PEP 517 backends, build orchestration, and crawl-indexing " + "extensions auto-loaded by gp-sphinx when ``docs_url`` is set." + ), ), ) @@ -1610,10 +1616,12 @@ def _live_signature_markdown(package_name: str) -> str: # in E2). Directives that emit H1 via parse_text_to_nodes do NOT # set the page title — Sphinx's title extraction has already run. lines = [ - f"Public callables in `{record.module_name}` rendered from the " - "running interpreter at docs-build time. Drift between this " - "block and the prose elsewhere on the page indicates a stale " - "docstring or signature comment.", + ( + f"Public callables in `{record.module_name}` rendered from the " + "running interpreter at docs-build time. Drift between this " + "block and the prose elsewhere on the page indicates a stale " + "docstring or signature comment." + ), "", ] for name, sig in pairs: @@ -1691,9 +1699,11 @@ def _kitchen_sink_markdown(package_name: str) -> str: # Body-only: stub supplies anchor + H1 so Sphinx finds a page title. lines = [ - "Every directive and role this package registers, exercised once " - "on the same page — useful as a reference card for downstream " - "authors and as a visual-regression target.", + ( + "Every directive and role this package registers, exercised once " + "on the same page — useful as a reference card for downstream " + "authors and as a visual-regression target." + ), "", ] if directives_seen: @@ -1813,9 +1823,11 @@ def _surface_changelog_markdown(package_name: str) -> str: # Body-only: stub supplies anchor + H1 so Sphinx finds a page title. lines = [ - "Comparison of the package's currently-registered directives, " - "roles, and config values against the snapshot stored at " - f"`docs/_static/surface-snapshots/{package_name}.json`.", + ( + "Comparison of the package's currently-registered directives, " + "roles, and config values against the snapshot stored at " + f"`docs/_static/surface-snapshots/{package_name}.json`." + ), "", ] if not snapshot_path.is_file(): @@ -1926,8 +1938,10 @@ def _package_dependents_markdown(package_name: str) -> str: dependents = _package_dependents(package_name) # Body-only: stub supplies anchor + H1 so Sphinx finds a page title. lines = [ - f"Workspace packages that declare a `{package_name}` dependency in " - "their `pyproject.toml` `[project].dependencies` array.", + ( + f"Workspace packages that declare a `{package_name}` dependency in " + "their `pyproject.toml` `[project].dependencies` array." + ), "", ] if not dependents: diff --git a/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py b/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py index de992cc9..6e574fc8 100644 --- a/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py +++ b/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py @@ -1304,8 +1304,10 @@ def _record_warning(message: str, *args: t.Any, **_kwargs: t.Any) -> None: assert result is None assert warning_calls == [ ( - "auto-pytest-plugin found no pytest fixtures in %r; " - "skipping generated fixture sections", + ( + "auto-pytest-plugin found no pytest fixtures in %r; " + "skipping generated fixture sections" + ), ("fixture_mod",), ) ] From 955c345946598769feda627a1c8978f9fc16bfec Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:11:01 -0500 Subject: [PATCH 16/21] chore(ruff[ignore]): Allow blind except at foreign boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Every BLE001 site in this workspace guards a call into code the extension does not own — introspecting a documented project's modules, stringifying whatever annotation autodoc supplies, resolving a builder URI, importing a user's argparse parser factory, reading intersphinx's untyped inventory. Each handler degrades to a fallback or reports a docutils error, so a single malformed object cannot abort the docs build. Narrowing the catches would let exotic exceptions from foreign code escape and do exactly that. what: - Per-file-ignore BLE001 in the six modules holding those boundaries https://docs.astral.sh/ruff/rules/blind-except/ --- pyproject.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7ad4289c..509386bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,6 +216,20 @@ convention = "numpy" "tests/ext/api_style/*.py" = ["D", "E501", "UP", "A", "B", "COM", "EM", "TRY", "PERF", "RUF", "SIM", "FA100"] "tests/ext/fastmcp/*.py" = ["D", "E501", "UP", "A", "B", "COM", "EM", "TRY", "PERF", "RUF", "SIM", "FA100"] "tests/ext/docutils/*.py" = ["D", "E501", "UP", "A", "B", "COM", "EM", "TRY", "PERF", "RUF", "SIM", "FA100"] +# BLE001: these modules sit on a boundary where the other side is +# arbitrary user or third-party code — introspecting a documented +# project's modules, stringifying whatever annotation autodoc hands +# over, asking Sphinx's builder to resolve a URI, importing a user's +# argparse parser factory, reading intersphinx's untyped inventory. +# Each catch degrades to a fallback or a reported docutils error so one +# bad object cannot abort the whole docs build; narrowing them would let +# an exotic exception from foreign code escape and do exactly that. +"packages/gp-sphinx/src/gp_sphinx/config.py" = ["BLE001"] +"packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/directive.py" = ["BLE001"] +"packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py" = ["BLE001"] +"packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py" = ["BLE001"] +"packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_store.py" = ["BLE001"] +"packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/rendering.py" = ["BLE001"] [tool.pytest.ini_options] addopts = "-s --tb=short --no-header --showlocals --doctest-modules" From e603341d5e3c2e2f2a4132721e429dd6ddb0ddd4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:11:17 -0500 Subject: [PATCH 17/21] chore(ruff[ignore]): Allow exec in module-synthesis tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Three suites build throwaway modules by compiling a literal source string, because the shapes under test cannot be written as ordinary imports: deferred annotations that must not evaluate, positional-only fixture parameters, `TYPE_CHECKING`-only aliases. The source is a literal in the test file — there is no untrusted input for S102 to protect against. what: - Per-file-ignore S102 in the three suites that synthesise modules https://docs.astral.sh/ruff/rules/exec-builtin/ --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 509386bc..8659bbc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -230,6 +230,14 @@ convention = "numpy" "packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py" = ["BLE001"] "packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_store.py" = ["BLE001"] "packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/rendering.py" = ["BLE001"] +# S102: these suites compile fixture, annotation, and dataclass source +# strings into throwaway modules so the extensions can be driven against +# shapes that only exist for the test — deferred annotations, positional +# -only fixture params, `TYPE_CHECKING` aliases. The source is a literal +# in the test file, not user input. +"tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py" = ["S102"] +"tests/ext/pytest_fixtures/test_type_checking_alias.py" = ["S102"] +"tests/ext/typehints_gp/test_documented_fields.py" = ["S102"] [tool.pytest.ini_options] addopts = "-s --tb=short --no-header --showlocals --doctest-modules" From e8c3301d0fd9e4836548434140718886eb948486 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:12:05 -0500 Subject: [PATCH 18/21] chore(ruff[ignore]): Allow the intersphinx probe to fall through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: `_resolve_builtin_link` asks intersphinx for a pytest fixture URL before falling back to the static `PYTEST_BUILTIN_LINKS` map. A failure inside the probe means the inventory has no usable answer — the same outcome as a plain miss, and the next statement already handles it. The `pass` is that fallthrough, not a swallowed error, and logging it would emit a warning in every docs build that no docs author can act on. what: - Add S110 alongside BLE001 on `_store.py` https://docs.astral.sh/ruff/rules/try-except-pass/ --- pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8659bbc6..75a72d92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,7 +228,13 @@ convention = "numpy" "packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/directive.py" = ["BLE001"] "packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py" = ["BLE001"] "packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py" = ["BLE001"] -"packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_store.py" = ["BLE001"] +# S110 rides along on `_store.py`: the intersphinx probe in +# `_resolve_builtin_link` is opportunistic, so a failure there means +# "the inventory has no usable answer" — the same outcome as a miss, and +# the very next statement supplies it from the static +# `PYTEST_BUILTIN_LINKS` map. The `pass` is that fallthrough; logging it +# would put a warning a docs author cannot act on into every build. +"packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_store.py" = ["BLE001", "S110"] "packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/rendering.py" = ["BLE001"] # S102: these suites compile fixture, annotation, and dataclass source # strings into throwaway modules so the extensions can be driven against From 861660f4b315fbcdb65a14e1c1e43b670e42b851 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:12:32 -0500 Subject: [PATCH 19/21] chore(ruff[ignore]): Keep the fixture-marker protocol private why: `_FixtureMarker` is the return type of `_detection._get_fixture_marker`, but PYI046 only looks inside the file that defines a protocol, so one that is private to the package rather than to the module reads as dead. Renaming it public would export an internal normalisation detail purely to satisfy the check. what: - Per-file-ignore PYI046 in `_models.py` https://docs.astral.sh/ruff/rules/unused-private-protocol/ --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 75a72d92..ed6bc233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -244,6 +244,12 @@ convention = "numpy" "tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py" = ["S102"] "tests/ext/pytest_fixtures/test_type_checking_alias.py" = ["S102"] "tests/ext/typehints_gp/test_documented_fields.py" = ["S102"] +# PYI046: `_FixtureMarker` is used — `_detection._get_fixture_marker` +# returns it — but the rule only looks inside the defining file, so a +# protocol that is private to the package rather than to the module +# reads as dead to it. Making it public would export an internal +# normalisation detail just to satisfy the check. +"packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_models.py" = ["PYI046"] [tool.pytest.ini_options] addopts = "-s --tb=short --no-header --showlocals --doctest-modules" From f43b5b18ff759a5a0f1f318a82ee42b69a50af95 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:13:06 -0500 Subject: [PATCH 20/21] chore(ruff[ignore]): Keep line-oriented joins as line lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Both FLY002 sites join a sequence of string literals where each literal is one output line — a Python program handed to a subprocess, and the expected RST block that `_build_autofixtures_directive_text` emits line by line. The one-literal-per-line shape is what makes them readable and reviewable; collapsing them into an f-string with embedded newlines (and, in the subprocess case, nested quoting) loses that. what: - Per-file-ignore FLY002 in `package_tools.py` and `test_sphinx_pytest_fixtures.py` https://docs.astral.sh/ruff/rules/static-join-to-f-string/ --- pyproject.toml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ed6bc233..92410129 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,7 +241,7 @@ convention = "numpy" # shapes that only exist for the test — deferred annotations, positional # -only fixture params, `TYPE_CHECKING` aliases. The source is a literal # in the test file, not user input. -"tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py" = ["S102"] +"tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures.py" = ["FLY002", "S102"] "tests/ext/pytest_fixtures/test_type_checking_alias.py" = ["S102"] "tests/ext/typehints_gp/test_documented_fields.py" = ["S102"] # PYI046: `_FixtureMarker` is used — `_detection._get_fixture_marker` @@ -250,6 +250,13 @@ convention = "numpy" # reads as dead to it. Making it public would export an internal # normalisation detail just to satisfy the check. "packages/sphinx-autodoc-pytest-fixtures/src/sphinx_autodoc_pytest_fixtures/_models.py" = ["PYI046"] +# FLY002: both joins assemble line-oriented text — a Python program fed +# to a subprocess here, and the expected RST block that +# `_build_autofixtures_directive_text` emits line by line in +# `test_sphinx_pytest_fixtures.py` above. One literal per output line is +# what keeps them readable and reviewable in a diff; an f-string +# carrying embedded newlines is neither. +"scripts/ci/package_tools.py" = ["FLY002"] [tool.pytest.ini_options] addopts = "-s --tb=short --no-header --showlocals --doctest-modules" From 600d7a091ff3bcfe3d43f520fccc76dc82e7824a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:15:07 -0500 Subject: [PATCH 21/21] docs(CHANGES): Note ruff's default rule set adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The changelog recorded the ruff floor but not the larger change it enabled — the workspace now lints against ruff's default rule set, so contributors see diagnostics from linters this project never named. what: - Extend the ruff 0.16 development entry with the default-set adoption and the scoped per-file ignores --- CHANGES | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 9552aeed..9b689978 100644 --- a/CHANGES +++ b/CHANGES @@ -29,7 +29,16 @@ which diagnostics fire. See the `ruff format` now formats Python code blocks embedded in Markdown, which reflows snippets in the READMEs and docs pages. Contributors who run the formatter on an older ruff will see those blocks revert, so upgrade -before committing. (#73) +before committing. + +Linting now runs ruff's default rule set alongside the linters this +workspace picks explicitly. An explicit `select` replaces that default +set rather than adding to it, so the config uses `extend-select`; the +set is much larger than the old selection, and contributors will see +diagnostics from linters the workspace never named. Rules that flag a +deliberate idiom — blind excepts guarding a docs build against foreign +code, `exec` in the suites that synthesise test modules — carry a scoped +per-file ignore with its reasoning in `pyproject.toml`. (#73) ## gp-sphinx 0.0.1a36 (2026-07-26)