Skip to content

Commit aea0569

Browse files
committed
pytest_plugin(fix[fixtures]): Import cleanly under pytest 9.1
why: pytest 9.1 rejects marks applied to fixture functions and raises the error at plugin import, before collection. Because libvcs ships this plugin as an installed pytest11 entry point, the error aborted the entire test session for any downstream project (e.g. vcspull) running pytest 9.1+. The skipif marks stacked on the fixtures were no-ops in every prior pytest version; the real gating must run inside each fixture so a missing binary skips instead of erroring. what: - Add private _skip_if_git_missing / _skip_if_svn_missing / _skip_if_hg_missing helpers that call pytest.skip() when the binary is absent; the svn helper requires both svn and svnadmin - Remove the @skip_if_*_missing decorators from the affected fixtures and call the matching helper as the first body statement - Fold empty_svn_repo's existing inline svn/svnadmin guard into the shared helper - Keep the public skip_if_*_missing marks: they remain valid for decorating test functions and importable by downstream suites
1 parent 77cb04f commit aea0569

1 file changed

Lines changed: 34 additions & 20 deletions

File tree

src/libvcs/pytest_plugin.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ def __init__(self, attempts: int, *args: object) -> None:
4343
)
4444

4545

46+
def _skip_if_git_missing() -> None:
47+
"""Skip the calling fixture when the ``git`` binary is unavailable."""
48+
if not shutil.which("git"):
49+
pytest.skip(reason="git is not available")
50+
51+
52+
def _skip_if_svn_missing() -> None:
53+
"""Skip the calling fixture when ``svn`` or ``svnadmin`` is unavailable."""
54+
if not shutil.which("svn") or not shutil.which("svnadmin"):
55+
pytest.skip(reason="svn is not available")
56+
57+
58+
def _skip_if_hg_missing() -> None:
59+
"""Skip the calling fixture when the ``hg`` binary is unavailable."""
60+
if not shutil.which("hg"):
61+
pytest.skip(reason="hg is not available")
62+
63+
4664
DEFAULT_VCS_NAME = "Test user"
4765
DEFAULT_VCS_EMAIL = "test@example.com"
4866

@@ -145,13 +163,13 @@ def set_home(
145163

146164

147165
@pytest.fixture(scope="session")
148-
@skip_if_git_missing
149166
def vcs_gitconfig(
150167
user_path: pathlib.Path,
151168
vcs_email: str,
152169
vcs_name: str,
153170
) -> pathlib.Path:
154171
"""Return git configuration, pytest fixture."""
172+
_skip_if_git_missing()
155173
gitconfig = user_path / ".gitconfig"
156174

157175
gitconfig.write_text(
@@ -173,24 +191,24 @@ def vcs_gitconfig(
173191

174192

175193
@pytest.fixture
176-
@skip_if_git_missing
177194
def set_vcs_gitconfig(
178195
monkeypatch: pytest.MonkeyPatch,
179196
vcs_gitconfig: pathlib.Path,
180197
) -> pathlib.Path:
181198
"""Set git configuration."""
199+
_skip_if_git_missing()
182200
monkeypatch.setenv("GIT_CONFIG", str(vcs_gitconfig))
183201
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(vcs_gitconfig)) # For child processes
184202
return vcs_gitconfig
185203

186204

187205
@pytest.fixture(scope="session")
188-
@skip_if_hg_missing
189206
def vcs_hgconfig(
190207
user_path: pathlib.Path,
191208
vcs_user: str,
192209
) -> pathlib.Path:
193210
"""Return Mercurial configuration."""
211+
_skip_if_hg_missing()
194212
hgrc = user_path / ".hgrc"
195213
hgrc.write_text(
196214
textwrap.dedent(
@@ -209,12 +227,12 @@ def vcs_hgconfig(
209227

210228

211229
@pytest.fixture
212-
@skip_if_hg_missing
213230
def set_vcs_hgconfig(
214231
monkeypatch: pytest.MonkeyPatch,
215232
vcs_hgconfig: pathlib.Path,
216233
) -> pathlib.Path:
217234
"""Set Mercurial configuration."""
235+
_skip_if_hg_missing()
218236
monkeypatch.setenv("HGRCPATH", str(vcs_hgconfig))
219237
return vcs_hgconfig
220238

@@ -338,11 +356,11 @@ def empty_git_bare_repo_path(libvcs_test_cache_path: pathlib.Path) -> pathlib.Pa
338356

339357

340358
@pytest.fixture(scope="session")
341-
@skip_if_git_missing
342359
def empty_git_bare_repo(
343360
empty_git_bare_repo_path: pathlib.Path,
344361
) -> pathlib.Path:
345362
"""Return factory to create git remote repo to for clone / push purposes."""
363+
_skip_if_git_missing()
346364
if (
347365
empty_git_bare_repo_path.exists()
348366
and (empty_git_bare_repo_path / ".git").exists()
@@ -357,11 +375,11 @@ def empty_git_bare_repo(
357375

358376

359377
@pytest.fixture(scope="session")
360-
@skip_if_git_missing
361378
def empty_git_repo(
362379
empty_git_repo_path: pathlib.Path,
363380
) -> pathlib.Path:
364381
"""Return factory to create git remote repo to for clone / push purposes."""
382+
_skip_if_git_missing()
365383
if empty_git_repo_path.exists() and (empty_git_repo_path / ".git").exists():
366384
return empty_git_repo_path
367385

@@ -373,12 +391,12 @@ def empty_git_repo(
373391

374392

375393
@pytest.fixture(scope="session")
376-
@skip_if_git_missing
377394
def create_git_remote_bare_repo(
378395
remote_repos_path: pathlib.Path,
379396
empty_git_bare_repo: pathlib.Path,
380397
) -> CreateRepoFn:
381398
"""Return factory to create git remote repo to for clone / push purposes."""
399+
_skip_if_git_missing()
382400

383401
def fn(
384402
remote_repos_path: pathlib.Path = remote_repos_path,
@@ -402,12 +420,12 @@ def fn(
402420

403421

404422
@pytest.fixture(scope="session")
405-
@skip_if_git_missing
406423
def create_git_remote_repo(
407424
remote_repos_path: pathlib.Path,
408425
empty_git_repo: pathlib.Path,
409426
) -> CreateRepoFn:
410427
"""Return factory to create git remote repo to for clone / push purposes."""
428+
_skip_if_git_missing()
411429

412430
def fn(
413431
remote_repos_path: pathlib.Path = remote_repos_path,
@@ -455,13 +473,13 @@ def git_remote_repo_single_commit_post_init(
455473

456474

457475
@pytest.fixture(scope="session")
458-
@skip_if_git_missing
459476
def git_remote_repo(
460477
create_git_remote_repo: CreateRepoFn,
461478
vcs_gitconfig: pathlib.Path,
462479
git_commit_envvars: GitCommitEnvVars,
463480
) -> pathlib.Path:
464481
"""Copy the session-scoped Git repository to a temporary directory."""
482+
_skip_if_git_missing()
465483
# TODO: Cache the effect of of this in a session-based repo
466484
repo_path = create_git_remote_repo()
467485
git_remote_repo_single_commit_post_init(
@@ -519,15 +537,11 @@ def empty_svn_repo_path(libvcs_test_cache_path: pathlib.Path) -> pathlib.Path:
519537

520538

521539
@pytest.fixture(scope="session")
522-
@skip_if_svn_missing
523540
def empty_svn_repo(
524541
empty_svn_repo_path: pathlib.Path,
525542
) -> pathlib.Path:
526543
"""Return factory to create svn remote repo to for clone / push purposes."""
527-
if not shutil.which("svn") or not shutil.which("svnadmin"):
528-
pytest.skip(
529-
reason="svn is not available",
530-
)
544+
_skip_if_svn_missing()
531545

532546
if empty_svn_repo_path.exists() and (empty_svn_repo_path / "conf").exists():
533547
return empty_svn_repo_path
@@ -540,12 +554,12 @@ def empty_svn_repo(
540554

541555

542556
@pytest.fixture(scope="session")
543-
@skip_if_svn_missing
544557
def create_svn_remote_repo(
545558
remote_repos_path: pathlib.Path,
546559
empty_svn_repo: pathlib.Path,
547560
) -> CreateRepoFn:
548561
"""Pre-made svn repo, bare, used as a file:// remote to checkout and commit to."""
562+
_skip_if_svn_missing()
549563

550564
def fn(
551565
remote_repos_path: pathlib.Path = remote_repos_path,
@@ -572,20 +586,20 @@ def fn(
572586

573587

574588
@pytest.fixture(scope="session")
575-
@skip_if_svn_missing
576589
def svn_remote_repo(
577590
create_svn_remote_repo: CreateRepoFn,
578591
) -> pathlib.Path:
579592
"""Pre-made. Local file:// based SVN server."""
593+
_skip_if_svn_missing()
580594
return create_svn_remote_repo()
581595

582596

583597
@pytest.fixture(scope="session")
584-
@skip_if_svn_missing
585598
def svn_remote_repo_with_files(
586599
create_svn_remote_repo: CreateRepoFn,
587600
) -> pathlib.Path:
588601
"""Pre-made. Local file:// based SVN server."""
602+
_skip_if_svn_missing()
589603
repo_path = create_svn_remote_repo()
590604
svn_remote_repo_single_commit_post_init(remote_repo_path=repo_path)
591605
return repo_path
@@ -629,11 +643,11 @@ def empty_hg_repo_path(libvcs_test_cache_path: pathlib.Path) -> pathlib.Path:
629643

630644

631645
@pytest.fixture(scope="session")
632-
@skip_if_hg_missing
633646
def empty_hg_repo(
634647
empty_hg_repo_path: pathlib.Path,
635648
) -> pathlib.Path:
636649
"""Return factory to create hg remote repo to for clone / push purposes."""
650+
_skip_if_hg_missing()
637651
if empty_hg_repo_path.exists() and (empty_hg_repo_path / ".hg").exists():
638652
return empty_hg_repo_path
639653

@@ -645,13 +659,13 @@ def empty_hg_repo(
645659

646660

647661
@pytest.fixture(scope="session")
648-
@skip_if_hg_missing
649662
def create_hg_remote_repo(
650663
remote_repos_path: pathlib.Path,
651664
empty_hg_repo: pathlib.Path,
652665
vcs_hgconfig: pathlib.Path,
653666
) -> CreateRepoFn:
654667
"""Pre-made hg repo, bare, used as a file:// remote to checkout and commit to."""
668+
_skip_if_hg_missing()
655669

656670
def fn(
657671
remote_repos_path: pathlib.Path = remote_repos_path,
@@ -681,13 +695,13 @@ def fn(
681695

682696

683697
@pytest.fixture(scope="session")
684-
@skip_if_hg_missing
685698
def hg_remote_repo(
686699
remote_repos_path: pathlib.Path,
687700
create_hg_remote_repo: CreateRepoFn,
688701
vcs_hgconfig: pathlib.Path,
689702
) -> pathlib.Path:
690703
"""Pre-made, file-based repo for push and pull."""
704+
_skip_if_hg_missing()
691705
repo_path = create_hg_remote_repo()
692706
hg_remote_repo_single_commit_post_init(
693707
remote_repo_path=repo_path,

0 commit comments

Comments
 (0)