From 4660181f1ac85cd9bb1c20a20ec6beec854b5ac4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:22:45 -0500 Subject: [PATCH 01/18] 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 0e2a15c4..82f47674 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,10 @@ sphinx-gp-sitemap = false sphinx-gp-theme = false sphinx-ux-autodoc-layout = false sphinx-ux-badges = 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 [tool.mypy] python_version = "3.10" From d15e371f88a6718f903e226f593f8a7c71ae9cfd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:23:02 -0500 Subject: [PATCH 02/18] py(deps[dev]): Require ruff>=0.16.0 why: ruff 0.16.0 stabilizes rules inside prefixes this project 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` and `lint` dependency groups - Relock `uv.lock`: ruff 0.15.22 -> 0.16.0 https://astral.sh/blog/ruff-v0.16.0 --- pyproject.toml | 4 ++-- uv.lock | 47 ++++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 82f47674..e13720f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ dev = [ "coverage", "pytest-cov", # Lint - "ruff", + "ruff>=0.16.0", "mypy", # Annotations "types-docutils", @@ -120,7 +120,7 @@ coverage =[ "pytest-cov", ] lint = [ - "ruff", + "ruff>=0.16.0", "mypy", ] typings = [ diff --git a/uv.lock b/uv.lock index 7bd57f5e..c1211b8a 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,7 @@ sphinx-ux-badges = false gp-libs = false sphinx-autodoc-docutils = false gp-furo-theme = false +ruff = false sphinx-gp-sitemap = false sphinx-fonts = false sphinx-autodoc-typehints-gp = false @@ -1081,27 +1082,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]] @@ -1763,7 +1764,7 @@ dev = [ { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "sphinx-autobuild" }, { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a36" }, { name = "sphinx-autodoc-argparse", specifier = "==0.0.1a36" }, @@ -1783,7 +1784,7 @@ docs = [ ] lint = [ { name = "mypy" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, ] testing = [ { name = "gp-libs", specifier = ">=0.0.19" }, From 8be72404095684edfe7825910b2cdd3445295645 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:24:15 -0500 Subject: [PATCH 03/18] docs(style[private_path]): Reformat Python code block why: ruff 0.16.0 formats Python code blocks inside Markdown by default, so `ruff format . --check` now fails CI on the hand-aligned inline comments in this doc. what: - Collapse comment padding to the two-space gap ruff format emits https://astral.sh/blog/ruff-v0.16.0 --- docs/internals/api/private_path.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/internals/api/private_path.md b/docs/internals/api/private_path.md index cd1be643..7eb369ef 100644 --- a/docs/internals/api/private_path.md +++ b/docs/internals/api/private_path.md @@ -18,8 +18,8 @@ internal logic. from vcspull._internal.private_path import PrivatePath home_repo = PrivatePath("~/code/vcspull") -print(home_repo) # -> ~/code/vcspull -print(repr(home_repo)) # -> "PrivatePath('~/code/vcspull')" +print(home_repo) # -> ~/code/vcspull +print(repr(home_repo)) # -> "PrivatePath('~/code/vcspull')" ``` ## Usage guidelines From e39ffc199b3c742d6ca452698418e0f1974393f7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:26:26 -0500 Subject: [PATCH 04/18] docs(CHANGES): Note ruff 0.16 floor (#568) why: The unreleased entry tracks dev-tooling floors so contributors know which ruff their formatter output is checked against. what: - Add a `### Development` deliverable for the `ruff>=0.16.0` floor and the new Markdown code-block formatting --- CHANGES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES b/CHANGES index 498f97ea..b4ad8a63 100644 --- a/CHANGES +++ b/CHANGES @@ -56,6 +56,12 @@ Workflow actions moved to their current major releases: `actions/checkout` v7, and `dorny/paths-filter` v4. Workflow behavior is unchanged, though setup-uv no longer prunes the uv cache, so the first run after this repopulates it. +#### Ruff floor raised to 0.16 (#568) + +Minimum `ruff>=0.16.0` (was unpinned). `ruff format` now formats Python code +blocks inside Markdown, so documentation examples are held to the same +formatting gate as source files. Lint diagnostics are unchanged. + ## vcspull v1.66.0 (2026-07-25) vcspull v1.66.0 lets you register a repository before you have cloned it. From 18aea8f5c1ce9ebaf72bb2698cc3ceb9c05a3332 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 14:22:43 -0500 Subject: [PATCH 05/18] 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 4660181f1ac85cd9bb1c20a20ec6beec854b5ac4. --- pyproject.toml | 4 ---- uv.lock | 1 - 2 files changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e13720f9..a96b2251 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,10 +160,6 @@ sphinx-gp-sitemap = false sphinx-gp-theme = false sphinx-ux-autodoc-layout = false sphinx-ux-badges = 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 [tool.mypy] python_version = "3.10" diff --git a/uv.lock b/uv.lock index c1211b8a..b863f4c6 100644 --- a/uv.lock +++ b/uv.lock @@ -24,7 +24,6 @@ sphinx-ux-badges = false gp-libs = false sphinx-autodoc-docutils = false gp-furo-theme = false -ruff = false sphinx-gp-sitemap = false sphinx-fonts = false sphinx-autodoc-typehints-gp = false From a3a29461385e6cdb2ff412f5e4614a2bc8fc1af1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:04:25 -0500 Subject: [PATCH 06/18] 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 project 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 a96b2251..87fcb2a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -209,7 +209,10 @@ exclude_lines = [ target-version = "py310" [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 @@ -226,7 +229,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 0c68f258988c8de93b4c0d987aeba01c8964074e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:07:10 -0500 Subject: [PATCH 07/18] test(remotes[conftest]): Drop redundant pass in __exit__ why: `MockHTTPResponse.__exit__` already has a docstring as its body, so the trailing `pass` is dead weight that ruff's default rule set now flags. what: - Remove the `pass` placeholder from `MockHTTPResponse.__exit__` https://docs.astral.sh/ruff/rules/unnecessary-placeholder/ --- tests/_internal/remotes/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/_internal/remotes/conftest.py b/tests/_internal/remotes/conftest.py index 57c65172..d05eaf50 100644 --- a/tests/_internal/remotes/conftest.py +++ b/tests/_internal/remotes/conftest.py @@ -38,7 +38,6 @@ def __enter__(self) -> MockHTTPResponse: def __exit__(self, *args: t.Any) -> None: """Context manager exit.""" - pass @pytest.fixture From 665c0967be523b29ba68c64034e8bbb2f856fd8b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:36 -0500 Subject: [PATCH 08/18] test(sync_progress[imports]): Drop empty TYPE_CHECKING block why: The `if t.TYPE_CHECKING:` block held nothing but `pass`, so the module carried a `typing` import solely to guard an empty branch. what: - Remove the empty type-checking block - Drop the now-unused `import typing as t` https://docs.astral.sh/ruff/rules/empty-type-checking-block/ --- tests/cli/test_sync_progress.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/cli/test_sync_progress.py b/tests/cli/test_sync_progress.py index af92f5aa..d85e9ed8 100644 --- a/tests/cli/test_sync_progress.py +++ b/tests/cli/test_sync_progress.py @@ -4,7 +4,6 @@ import io import time -import typing as t import pytest @@ -13,9 +12,6 @@ build_indicator, ) -if t.TYPE_CHECKING: - pass - def test_disabled_indicator_is_silent_noop() -> None: """An indicator with ``enabled=False`` writes nothing to the stream.""" From 112ce7c3c4f03b4e2d4e694415beec0f8082462a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:40 -0500 Subject: [PATCH 09/18] style(strings): Parenthesize concatenations in literals why: A multi-part string sitting unparenthesized inside a list or tuple reads the same as a missing comma between two separate elements. Wrapping each one makes the joins explicit and keeps a stray comma from silently splitting the value into two. what: - Parenthesize the label-matching regex in the output lexer's token table - Parenthesize the sync summary line built inside the `parts` list https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/ --- docs/_ext/vcspull_output_lexer.py | 6 ++++-- src/vcspull/cli/sync.py | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/_ext/vcspull_output_lexer.py b/docs/_ext/vcspull_output_lexer.py index 46115bd0..d8796fa8 100644 --- a/docs/_ext/vcspull_output_lexer.py +++ b/docs/_ext/vcspull_output_lexer.py @@ -112,8 +112,10 @@ class VcspullOutputLexer(RegexLexer): (r"\bfailed\b", Generic.Error), # Labels (muted) - common vcspull output labels ( - r"(Summary:|Progress:|Path:|Branch:|url:|workspace:|Ahead/Behind:|" - r"Remote:|Repository:|Note:|Usage:|Plan:|Tip:)", + ( + r"(Summary:|Progress:|Path:|Branch:|url:|workspace:|Ahead/Behind:|" + r"Remote:|Repository:|Note:|Usage:|Plan:|Tip:)" + ), Generic.Heading, ), # vcspull command and subcommands (for pretty docs) diff --git a/src/vcspull/cli/sync.py b/src/vcspull/cli/sync.py index 00cf774f..059956c7 100644 --- a/src/vcspull/cli/sync.py +++ b/src/vcspull/cli/sync.py @@ -1983,10 +1983,12 @@ def _emit_summary( unmatched = summary.get("unmatched", 0) timed_out = summary.get("timed_out", 0) parts = [ - f"\n{colors.info('Summary:')} " - f"{colors.info(str(summary['total']))} repos, " - f"{colors.success(str(summary['synced']))} synced, " - f"{colors.error(str(summary['failed']))} failed", + ( + f"\n{colors.info('Summary:')} " + f"{colors.info(str(summary['total']))} repos, " + f"{colors.success(str(summary['synced']))} synced, " + f"{colors.error(str(summary['failed']))} failed" + ), ] if timed_out > 0: parts.append( From 5d0b13726d3aaa77b578da244488e4cbbccb5b29 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:10:01 -0500 Subject: [PATCH 10/18] test(subprocess): State check=False where exit code is asserted why: Both call sites deliberately let git fail so the test can inspect `returncode` itself -- one asserts a clean-environment commit succeeds with git's stderr in the message, the other probes whether an `origin` remote exists. Spelling `check` out separates that intent from an oversight. what: - Pass `check=False` to the `git commit` probe in the fixture test - Pass `check=False` to the `git remote get-url origin` probe https://docs.astral.sh/ruff/rules/subprocess-run-without-check/ --- tests/test_fixtures.py | 1 + tests/test_worktree.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 7279f35b..37ea930b 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -71,6 +71,7 @@ def test_git_commit_works_in_subprocess( cwd=repo_dir, capture_output=True, text=True, + check=False, ) assert result.returncode == 0, ( f"git commit failed: {result.stderr}\n" diff --git a/tests/test_worktree.py b/tests/test_worktree.py index 243c11ec..fca73312 100644 --- a/tests/test_worktree.py +++ b/tests/test_worktree.py @@ -2546,6 +2546,7 @@ def test_ref_exists_remote_branch_fallback( ["git", "remote", "get-url", "origin"], cwd=git_repo.path, capture_output=True, + check=False, ) if result.returncode == 0: subprocess.run( From b2626fb19b3b4da003a23a8b2d9008304df6183f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:11:11 -0500 Subject: [PATCH 11/18] cli(sync[exc]): Drop return from exception __init__ why: `CouldNotGuessVCSFromURL.__init__` returned the result of `super().__init__(...)`. A constructor may only return `None`, so the `return` conveyed nothing and would raise `TypeError` if the base ever returned a value. what: - Call `super().__init__(...)` as a statement instead of returning it https://docs.astral.sh/ruff/rules/return-in-init/ --- src/vcspull/cli/sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vcspull/cli/sync.py b/src/vcspull/cli/sync.py index 059956c7..6a3c614f 100644 --- a/src/vcspull/cli/sync.py +++ b/src/vcspull/cli/sync.py @@ -2043,7 +2043,7 @@ class CouldNotGuessVCSFromURL(exc.VCSPullException): """Raised when no VCS could be guessed from a URL.""" def __init__(self, repo_url: str, *args: object, **kwargs: object) -> None: - return super().__init__(f"Could not automatically determine VCS for {repo_url}") + super().__init__(f"Could not automatically determine VCS for {repo_url}") class SyncFailedError(exc.VCSPullException): From 6af7f51ba4b051cbe5e82ea764e2ff33d283f8c4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:12:39 -0500 Subject: [PATCH 12/18] log(debug_path[stamp]): Make the log timestamp tz-aware why: The per-invocation debug log file name is stamped from `datetime.now()`, which returns a naive value whose meaning depends on the ambient system zone. Attaching the local zone keeps the file name digits the reader expects while making the instant unambiguous. what: - Chain `.astimezone()` onto `datetime.now()` in `default_debug_log_path` - Teach the timestamp test double to answer `astimezone` https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/ --- src/vcspull/log.py | 2 +- tests/cli/test_sync_log_file.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vcspull/log.py b/src/vcspull/log.py index beb9b3c0..da66ad8a 100644 --- a/src/vcspull/log.py +++ b/src/vcspull/log.py @@ -377,7 +377,7 @@ def default_debug_log_path() -> pathlib.Path: >>> path.parent.parent == pathlib.Path(tempfile.gettempdir()) True """ - stamp = datetime.now().strftime("%Y%m%dT%H%M%S") + stamp = datetime.now().astimezone().strftime("%Y%m%dT%H%M%S") pid = os.getpid() subdir = "vcspull-test" if "PYTEST_CURRENT_TEST" in os.environ else "vcspull" return pathlib.Path(tempfile.gettempdir()) / subdir / f"debug-{stamp}-{pid}.log" diff --git a/tests/cli/test_sync_log_file.py b/tests/cli/test_sync_log_file.py index 150b3407..1152ef47 100644 --- a/tests/cli/test_sync_log_file.py +++ b/tests/cli/test_sync_log_file.py @@ -138,6 +138,9 @@ def now(cls) -> t.Any: cls.value += 1 class _Stamp: + def astimezone(self) -> t.Any: + return self + @staticmethod def strftime(_fmt: str) -> str: return f"2026042400000{_Fake.value}" From 8c5007895f3b665140e7352de70621a9d6346110 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:13:51 -0500 Subject: [PATCH 13/18] types(__exit__): Annotate context-manager exit params why: Both `__exit__` implementations typed their tail parameters as `t.Any`, which accepts anything and tells a reader nothing about the protocol. Naming the real shapes lets a type checker catch a caller that passes the wrong triple. what: - Type `_RepoContext.__exit__`'s traceback as `types.TracebackType | None` - Type `MockHTTPResponse.__exit__`'s star-args as `object` https://docs.astral.sh/ruff/rules/bad-exit-annotation/ --- src/vcspull/cli/_progress.py | 5 ++++- tests/_internal/remotes/conftest.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vcspull/cli/_progress.py b/src/vcspull/cli/_progress.py index 555ac23a..f80c6d0e 100644 --- a/src/vcspull/cli/_progress.py +++ b/src/vcspull/cli/_progress.py @@ -24,6 +24,9 @@ from ._colors import ColorMode, Colors, get_color_mode +if t.TYPE_CHECKING: + import types + log = logging.getLogger(__name__) @@ -588,7 +591,7 @@ def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, - tb: t.Any, + tb: types.TracebackType | None, ) -> None: self._indicator.stop_repo() diff --git a/tests/_internal/remotes/conftest.py b/tests/_internal/remotes/conftest.py index d05eaf50..63f4189b 100644 --- a/tests/_internal/remotes/conftest.py +++ b/tests/_internal/remotes/conftest.py @@ -36,7 +36,7 @@ def __enter__(self) -> MockHTTPResponse: """Context manager entry.""" return self - def __exit__(self, *args: t.Any) -> None: + def __exit__(self, *args: object) -> None: """Context manager exit.""" From b6d6b7424c4b44ae73a8edd75b5218c036d6ab91 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:15:22 -0500 Subject: [PATCH 14/18] types(self): Return Self from __new__ and __enter__ why: `PrivatePath.__new__` and the two `__enter__` methods were annotated with their own concrete class, so a subclass instance was inferred as the base -- `with SubContext(...) as ctx` lost every attribute the subclass added. `Self` binds the return to the actual class. what: - Return `Self` from `PrivatePath.__new__` - Return `Self` from `_RepoContext.__enter__` and `MockHTTPResponse.__enter__` - Import `Self` from `typing_extensions` under `TYPE_CHECKING`, since `typing.Self` needs 3.11 and the package floor is 3.10 https://docs.astral.sh/ruff/rules/non-self-return-type/ --- src/vcspull/_internal/private_path.py | 4 +++- src/vcspull/cli/_progress.py | 4 +++- tests/_internal/remotes/conftest.py | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vcspull/_internal/private_path.py b/src/vcspull/_internal/private_path.py index 367d3dfd..e42e4a08 100644 --- a/src/vcspull/_internal/private_path.py +++ b/src/vcspull/_internal/private_path.py @@ -5,6 +5,8 @@ import typing as t if t.TYPE_CHECKING: + from typing_extensions import Self + PrivatePathBase = pathlib.Path else: PrivatePathBase = type(pathlib.Path()) @@ -34,7 +36,7 @@ class PrivatePath(PrivatePathBase): '~/notes.txt' """ - def __new__(cls, *args: t.Any, **kwargs: t.Any) -> PrivatePath: + def __new__(cls, *args: t.Any, **kwargs: t.Any) -> Self: return super().__new__(cls, *args, **kwargs) @classmethod diff --git a/src/vcspull/cli/_progress.py b/src/vcspull/cli/_progress.py index f80c6d0e..acb40937 100644 --- a/src/vcspull/cli/_progress.py +++ b/src/vcspull/cli/_progress.py @@ -27,6 +27,8 @@ if t.TYPE_CHECKING: import types + from typing_extensions import Self + log = logging.getLogger(__name__) @@ -583,7 +585,7 @@ def __init__(self, indicator: SyncStatusIndicator, name: str) -> None: self._indicator = indicator self._name = name - def __enter__(self) -> _RepoContext: + def __enter__(self) -> Self: self._indicator.start_repo(self._name) return self diff --git a/tests/_internal/remotes/conftest.py b/tests/_internal/remotes/conftest.py index 63f4189b..be23f8e1 100644 --- a/tests/_internal/remotes/conftest.py +++ b/tests/_internal/remotes/conftest.py @@ -8,6 +8,9 @@ import pytest +if t.TYPE_CHECKING: + from typing_extensions import Self + class MockHTTPResponse: """Mock HTTP response for testing.""" @@ -32,7 +35,7 @@ def getheaders(self) -> list[tuple[str, str]]: """Return response headers as list of tuples.""" return list(self._headers.items()) - def __enter__(self) -> MockHTTPResponse: + def __enter__(self) -> Self: """Context manager entry.""" return self From e572f17485e118a02019821d9c8d1faee211dcb0 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:15:39 -0500 Subject: [PATCH 15/18] chore(ruff[S102]): Allow exec in the Sphinx conf why: docs/conf.py reads the version, title, and repo URLs by exec'ing src/vcspull/__about__.py -- the standard Sphinx way to pull metadata without importing (and therefore installing) the package. The exec'd input is a file in this repository, not user data. what: - Per-file-ignore S102 for docs/conf.py, with the reason in the config https://docs.astral.sh/ruff/rules/exec-builtin/ --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 87fcb2a0..b8332c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,6 +260,10 @@ required-imports = [ [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401"] +# Sphinx conf.py reads the package's version metadata by exec'ing +# src/vcspull/__about__.py, so conf.py stays importable without the +# package installed. The input is a file in this repository. +"docs/conf.py" = ["S102"] [tool.pytest.ini_options] addopts = "--tb=short --no-header --showlocals --doctest-modules" From e760205df4a8f04349f54e6d5afb0b7adb76a316 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:16:15 -0500 Subject: [PATCH 16/18] chore(ruff[BLE001]): Allow the boundary catch-alls why: Every blind `except` left in the tree guards a boundary where the alternative is worse than a broad catch: an atexit handler whose raise the interpreter swallows, a sync worker thread that must hand its failure back to the main thread, a log formatter that would take the application down, and a smoke test whose entire purpose is reporting arbitrary import failures. Each already carries a comment saying so. what: - Per-file-ignore BLE001 for the four files, each with its reason in the config https://docs.astral.sh/ruff/rules/blind-except/ --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b8332c39..d5ffeaa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -264,6 +264,22 @@ required-imports = [ # src/vcspull/__about__.py, so conf.py stays importable without the # package installed. The input is a file in this repository. "docs/conf.py" = ["S102"] +# The smoke test's job is to report any failure from importing an +# arbitrary module or invoking the CLI, so the catch has to be as wide +# as the failures it is probing for. +"scripts/runtime_dep_smoketest.py" = ["BLE001"] +# atexit handlers that raise are swallowed by the interpreter, so the +# cursor-restore handler logs and swallows to leave a breadcrumb +# without masking other shutdown tasks. +"src/vcspull/cli/_progress.py" = ["BLE001"] +# The sync worker thread catches everything, including BaseException, +# to hand the failure back to the main thread; narrowing would lose +# the exception instead of reporting it. +"src/vcspull/cli/sync.py" = ["BLE001"] +# A log formatter that raises takes the application down with it, so +# %-format failures in a caller's record become a "Bad message" line +# rather than an exception. +"src/vcspull/log.py" = ["BLE001"] [tool.pytest.ini_options] addopts = "--tb=short --no-header --showlocals --doctest-modules" From 20ab98e4e0c0829b6b18a2dad3e9bac2f7b2fc14 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:16:55 -0500 Subject: [PATCH 17/18] docs(CHANGES): Note default rule set adoption why: The ruff 0.16 entry claimed lint diagnostics were unchanged, which stopped being true once the project stopped replacing ruff's default rule set. what: - Describe the default rule set adoption and the two scoped ignores - Drop the "lint diagnostics are unchanged" claim --- CHANGES | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index b4ad8a63..c729c3c8 100644 --- a/CHANGES +++ b/CHANGES @@ -60,7 +60,14 @@ longer prunes the uv cache, so the first run after this repopulates it. Minimum `ruff>=0.16.0` (was unpinned). `ruff format` now formats Python code blocks inside Markdown, so documentation examples are held to the same -formatting gate as source files. Lint diagnostics are unchanged. +formatting gate as source files. + +Linting also adopts ruff's curated default rule set, which the project's +explicit `select` had been replacing rather than extending. Its findings are +fixed rather than silenced, with two exceptions kept as per-file ignores that +carry their reason in `pyproject.toml`: the Sphinx `conf.py` `exec` that reads +version metadata, and the deliberate catch-alls guarding the sync worker +thread, the log formatter, interpreter shutdown, and the dependency smoke test. ## vcspull v1.66.0 (2026-07-25) From b042904de73c297249e982cb021601bd56312312 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:21:38 -0500 Subject: [PATCH 18/18] py(vcspull[init]): Drop the vestigial shebang why: src/vcspull/__init__.py carried `#!/usr/bin/env python` but is a package initializer, never run as a script, and is not executable. The shebang promises something the file cannot do. what: - Remove the shebang line from the package initializer https://docs.astral.sh/ruff/rules/shebang-not-executable/ --- src/vcspull/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vcspull/__init__.py b/src/vcspull/__init__.py index 2d834a11..d8fd6106 100644 --- a/src/vcspull/__init__.py +++ b/src/vcspull/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Manage multiple git, mercurial, svn repositories from a YAML / JSON file. :copyright: Copyright 2013-2018 Tony Narlock.