From 526be290ed6d88f5882c74391967f01cdee01650 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2026 06:22:20 -0500 Subject: [PATCH 1/3] pytest_plugin(fix[pytest_ignore_collect]): abstain with None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: pytest_ignore_collect is a firstresult hook, so the first non-None return wins and short-circuits the remaining implementations. The fall-through returned False, silently overriding gp-libs' Sphinx _build skip and pytest's builtin __pycache__/norecursedirs/collect_ignore handling — aborting collection on generated docs under docs/_build. what: - Return None to abstain on paths with no missing VCS binary - Widen the return annotation to bool | None Refs #543 --- src/libvcs/pytest_plugin.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/libvcs/pytest_plugin.py b/src/libvcs/pytest_plugin.py index a80f9e7e..e4a6fe4d 100644 --- a/src/libvcs/pytest_plugin.py +++ b/src/libvcs/pytest_plugin.py @@ -119,18 +119,30 @@ def __next__(self) -> str: namer = RandomStrSequence() -def pytest_ignore_collect(collection_path: pathlib.Path, config: pytest.Config) -> bool: - """Skip tests if VCS binaries are missing.""" +def pytest_ignore_collect( + collection_path: pathlib.Path, + config: pytest.Config, +) -> bool | None: + """Skip collection of tests that require a missing VCS binary. + + ``pytest_ignore_collect`` is a ``firstresult`` hook: the first + implementation to return a non-``None`` value wins and short-circuits the + rest. This hook must therefore return ``None`` (abstain) for paths it has + no opinion on, so that other implementations -- gp-libs' Sphinx ``_build`` + skip and pytest's own ``__pycache__``/``norecursedirs``/``collect_ignore`` + handling -- still run. Returning ``False`` here would suppress all of them. + """ if (not shutil.which("svn") or not shutil.which("svnadmin")) and any( needle in str(collection_path) for needle in ["svn", "subversion"] ): return True if not shutil.which("git") and "git" in str(collection_path): return True - return bool( - not shutil.which("hg") - and any(needle in str(collection_path) for needle in ["hg", "mercurial"]), - ) + if not shutil.which("hg") and any( + needle in str(collection_path) for needle in ["hg", "mercurial"] + ): + return True + return None @pytest.fixture(scope="session") From 44161abf13d87accc4827db1de947b07ec530717 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2026 06:22:20 -0500 Subject: [PATCH 2/3] pytest_plugin(test): cover ignore_collect abstain why: Lock in that the firstresult hook abstains with None rather than voting False, so it no longer suppresses other collection-ignore hooks. what: - Abstain (None) on a non-VCS path regardless of installed binaries - Ignore (True) when a VCS binary is missing - Abstain (None) when the binary is present Refs #543 --- tests/test_pytest_plugin.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index e024d8c1..6c9373d2 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -11,6 +11,7 @@ import pytest from libvcs._internal.run import run +from libvcs.pytest_plugin import pytest_ignore_collect if t.TYPE_CHECKING: import pathlib @@ -311,3 +312,47 @@ def test_git_repo_fixture_submodule_file_protocol( f"git submodule add failed: {result}\n" "git_repo fixture needs set_home dependency for child processes" ) + + +def test_pytest_ignore_collect_abstains_on_non_vcs_path( + pytestconfig: pytest.Config, + tmp_path: pathlib.Path, +) -> None: + """Non-VCS paths abstain (``None``) rather than voting ``False``. + + ``pytest_ignore_collect`` is a ``firstresult`` hook, so returning ``False`` + would short-circuit the chain and suppress gp-libs' Sphinx ``_build`` skip + and pytest's builtin ignores. A path mentioning no VCS must yield ``None`` + regardless of which VCS binaries are installed. + """ + build_artifact = tmp_path / "docs" / "_build" / "html" / "history.md" + + assert pytest_ignore_collect(build_artifact, config=pytestconfig) is None + + +def test_pytest_ignore_collect_ignores_missing_vcs( + pytestconfig: pytest.Config, + monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, +) -> None: + """Paths for a missing VCS binary are ignored (``True``).""" + monkeypatch.setattr( + shutil, + "which", + lambda cmd, *args, **kwargs: None if cmd == "git" else f"/usr/bin/{cmd}", + ) + git_test = tmp_path / "tests" / "sync" / "test_git.py" + + assert pytest_ignore_collect(git_test, config=pytestconfig) is True + + +def test_pytest_ignore_collect_abstains_when_binaries_present( + pytestconfig: pytest.Config, + monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, +) -> None: + """A VCS path abstains when its binary is present, deferring to others.""" + monkeypatch.setattr(shutil, "which", lambda cmd, *args, **kwargs: f"/usr/bin/{cmd}") + git_test = tmp_path / "tests" / "sync" / "test_git.py" + + assert pytest_ignore_collect(git_test, config=pytestconfig) is None From f7d7c0e38a14e844b28dbd8e91c2af7784598e8d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2026 06:26:02 -0500 Subject: [PATCH 3/3] docs(CHANGES): pytest plugin collection-ignore fix why: Record the unreleased pytest-plugin fix so downstream users see it in the next release notes. --- CHANGES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES b/CHANGES index ed81d385..24f46ca3 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,12 @@ $ uv add libvcs --prerelease allow _Notes on the upcoming release will go here._ +### Fixes + +- libvcs's bundled pytest plugin no longer overrides pytest's own + collection-ignore rules; suites that build their docs before running + no longer abort on the generated `_build/` output (#544). + ### Documentation #### Documentation examples now run as doctests (#540)