Skip to content
Merged
21 changes: 21 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@ $ uv add libvcs --prerelease allow
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### pytest plugin: Improve typings (#521)

The pytest plugin now exports concise public TypeAliases in place of
the private `_ENV` type that was leaking into public signatures:

- `Env` — public alias for the subprocess environment mapping type;
replaces `_ENV` in all `env:` parameters across Protocol classes and
helper functions
- `GitCommitEnvVars` — alias for `dict[str, str]`, the type returned
by the `git_commit_envvars` fixture

`CreateRepoPytestFixtureFn` is renamed to `CreateRepoFn`. Update any
`TYPE_CHECKING` imports accordingly.

### Documentation

- pytest plugin: `CreateRepoFn` and `CreateRepoPostInitFn` now render
as hyperlinks in the fixture summary table (#521)

## libvcs 0.39.0 (2026-02-07)

### New features
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ Writing a tool that interacts with VCS? Use our fixtures to keep your tests clea

```python
import pathlib
from libvcs.pytest_plugin import CreateRepoPytestFixtureFn
from libvcs.pytest_plugin import CreateRepoFn
from libvcs.sync.git import GitSync

def test_my_git_tool(
create_git_remote_repo: CreateRepoPytestFixtureFn,
create_git_remote_repo: CreateRepoFn,
tmp_path: pathlib.Path
):
# Spin up a real, temporary Git server
Expand Down
14 changes: 14 additions & 0 deletions docs/api/pytest-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,17 @@ def setup(
pass
```
:::

## Types

```{eval-rst}
.. autodata:: libvcs.pytest_plugin.GitCommitEnvVars

.. autoclass:: libvcs.pytest_plugin.CreateRepoFn
:special-members: __call__
:exclude-members: __init__, _abc_impl, _is_protocol

.. autoclass:: libvcs.pytest_plugin.CreateRepoPostInitFn
:special-members: __call__
:exclude-members: __init__, _abc_impl, _is_protocol
```
38 changes: 38 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

import pathlib
import sys
import typing as t

if t.TYPE_CHECKING:
from docutils import nodes # type: ignore[import-untyped]
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.domains.python import PythonDomain
from sphinx.environment import BuildEnvironment

from gp_sphinx.config import make_linkcode_resolve, merge_sphinx_config

Expand Down Expand Up @@ -54,3 +62,33 @@
rediraffe_redirects="redirects.txt",
)
globals().update(conf)


def _on_missing_class_reference(
app: Sphinx,
env: BuildEnvironment,
node: addnodes.pending_xref,
contnode: nodes.TextElement,
) -> nodes.reference | None:
if node.get("refdomain") != "py" or node.get("reftype") != "class":
return None
from sphinx.util.nodes import make_refnode

py_domain: PythonDomain = env.get_domain("py") # type: ignore[assignment]
target = node.get("reftarget", "")
matches = py_domain.find_obj(env, "", "", target, None, 1) # type: ignore[attr-defined,unused-ignore]
if not matches:
return None
_name, obj_entry = matches[0]
return make_refnode(
app.builder,
node.get("refdoc", ""),
obj_entry.docname,
obj_entry.node_id,
contnode,
)


def setup(app: Sphinx) -> None:
"""Connect missing-reference handler to resolve py:data as :class: links."""
app.connect("missing-reference", _on_missing_class_reference)
43 changes: 23 additions & 20 deletions src/libvcs/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def vcs_user(vcs_name: str, vcs_email: str) -> str:


@pytest.fixture(scope="session")
def git_commit_envvars(vcs_name: str, vcs_email: str) -> _ENV:
def git_commit_envvars(vcs_name: str, vcs_email: str) -> GitCommitEnvVars:
"""Return environment variables for `git commit`.

For some reason, `GIT_CONFIG` via {func}`set_gitconfig` doesn't work for `git
Expand Down Expand Up @@ -266,6 +266,9 @@ def unique_repo_name(remote_repos_path: pathlib.Path, max_retries: int = 15) ->


InitCmdArgs: t.TypeAlias = list[str] | None
GitCommitEnvVars: t.TypeAlias = dict[str, str]
"""Environment variable mapping passed to ``git commit`` subprocess calls."""
Env: t.TypeAlias = _ENV


class CreateRepoPostInitFn(t.Protocol):
Expand All @@ -274,13 +277,13 @@ class CreateRepoPostInitFn(t.Protocol):
def __call__(
self,
remote_repo_path: pathlib.Path,
env: _ENV | None = None,
env: Env | None = None,
) -> None:
"""Ran after creating a repo from pytest fixture."""
...


class CreateRepoPytestFixtureFn(t.Protocol):
class CreateRepoFn(t.Protocol):
"""Typing for VCS pytest fixture callback."""

def __call__(
Expand All @@ -301,7 +304,7 @@ def _create_git_remote_repo(
remote_repo_path: pathlib.Path,
remote_repo_post_init: CreateRepoPostInitFn | None = None,
init_cmd_args: InitCmdArgs = DEFAULT_GIT_REMOTE_REPO_CMD_ARGS,
env: _ENV | None = None,
env: Env | None = None,
) -> pathlib.Path:
if init_cmd_args is None:
init_cmd_args = []
Expand Down Expand Up @@ -374,7 +377,7 @@ def empty_git_repo(
def create_git_remote_bare_repo(
remote_repos_path: pathlib.Path,
empty_git_bare_repo: pathlib.Path,
) -> CreateRepoPytestFixtureFn:
) -> CreateRepoFn:
"""Return factory to create git remote repo to for clone / push purposes."""

def fn(
Expand Down Expand Up @@ -403,7 +406,7 @@ def fn(
def create_git_remote_repo(
remote_repos_path: pathlib.Path,
empty_git_repo: pathlib.Path,
) -> CreateRepoPytestFixtureFn:
) -> CreateRepoFn:
"""Return factory to create git remote repo to for clone / push purposes."""

def fn(
Expand Down Expand Up @@ -434,7 +437,7 @@ def fn(

def git_remote_repo_single_commit_post_init(
remote_repo_path: pathlib.Path,
env: _ENV | None = None,
env: Env | None = None,
) -> None:
"""Post-initialization: Create a test git repo with a single commit."""
testfile_filename = "testfile.test"
Expand All @@ -454,9 +457,9 @@ def git_remote_repo_single_commit_post_init(
@pytest.fixture(scope="session")
@skip_if_git_missing
def git_remote_repo(
create_git_remote_repo: CreateRepoPytestFixtureFn,
create_git_remote_repo: CreateRepoFn,
gitconfig: pathlib.Path,
git_commit_envvars: _ENV,
git_commit_envvars: GitCommitEnvVars,
) -> pathlib.Path:
"""Copy the session-scoped Git repository to a temporary directory."""
# TODO: Cache the effect of of this in a session-based repo
Expand Down Expand Up @@ -490,7 +493,7 @@ def _create_svn_remote_repo(

def svn_remote_repo_single_commit_post_init(
remote_repo_path: pathlib.Path,
env: _ENV | None = None,
env: Env | None = None,
) -> None:
"""Post-initialization: Create a test SVN repo with a single commit."""
assert remote_repo_path.exists()
Expand Down Expand Up @@ -541,7 +544,7 @@ def empty_svn_repo(
def create_svn_remote_repo(
remote_repos_path: pathlib.Path,
empty_svn_repo: pathlib.Path,
) -> CreateRepoPytestFixtureFn:
) -> CreateRepoFn:
"""Pre-made svn repo, bare, used as a file:// remote to checkout and commit to."""

def fn(
Expand Down Expand Up @@ -571,7 +574,7 @@ def fn(
@pytest.fixture(scope="session")
@skip_if_svn_missing
def svn_remote_repo(
create_svn_remote_repo: CreateRepoPytestFixtureFn,
create_svn_remote_repo: CreateRepoFn,
) -> pathlib.Path:
"""Pre-made. Local file:// based SVN server."""
return create_svn_remote_repo()
Expand All @@ -580,7 +583,7 @@ def svn_remote_repo(
@pytest.fixture(scope="session")
@skip_if_svn_missing
def svn_remote_repo_with_files(
create_svn_remote_repo: CreateRepoPytestFixtureFn,
create_svn_remote_repo: CreateRepoFn,
) -> pathlib.Path:
"""Pre-made. Local file:// based SVN server."""
repo_path = create_svn_remote_repo()
Expand Down Expand Up @@ -610,7 +613,7 @@ def _create_hg_remote_repo(

def hg_remote_repo_single_commit_post_init(
remote_repo_path: pathlib.Path,
env: _ENV | None = None,
env: Env | None = None,
) -> None:
"""Post-initialization: Create a test mercurial repo with a single commit."""
testfile_filename = "testfile.test"
Expand Down Expand Up @@ -647,7 +650,7 @@ def create_hg_remote_repo(
remote_repos_path: pathlib.Path,
empty_hg_repo: pathlib.Path,
hgconfig: pathlib.Path,
) -> CreateRepoPytestFixtureFn:
) -> CreateRepoFn:
"""Pre-made hg repo, bare, used as a file:// remote to checkout and commit to."""

def fn(
Expand Down Expand Up @@ -681,7 +684,7 @@ def fn(
@skip_if_hg_missing
def hg_remote_repo(
remote_repos_path: pathlib.Path,
create_hg_remote_repo: CreateRepoPytestFixtureFn,
create_hg_remote_repo: CreateRepoFn,
hgconfig: pathlib.Path,
) -> pathlib.Path:
"""Pre-made, file-based repo for push and pull."""
Expand Down Expand Up @@ -787,11 +790,11 @@ def add_doctest_fixtures(
doctest_namespace: dict[str, t.Any],
tmp_path: pathlib.Path,
set_home: pathlib.Path,
git_commit_envvars: _ENV,
git_commit_envvars: GitCommitEnvVars,
hgconfig: pathlib.Path,
create_git_remote_repo: CreateRepoPytestFixtureFn,
create_svn_remote_repo: CreateRepoPytestFixtureFn,
create_hg_remote_repo: CreateRepoPytestFixtureFn,
create_git_remote_repo: CreateRepoFn,
create_svn_remote_repo: CreateRepoFn,
create_hg_remote_repo: CreateRepoFn,
git_repo: pathlib.Path,
) -> None:
"""Harness pytest fixtures to pytest's doctest namespace."""
Expand Down
6 changes: 3 additions & 3 deletions tests/cmd/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from libvcs.cmd import git

if t.TYPE_CHECKING:
from libvcs.pytest_plugin import CreateRepoPytestFixtureFn
from libvcs.pytest_plugin import CreateRepoFn, GitCommitEnvVars
from libvcs.sync.git import GitSync


Expand Down Expand Up @@ -956,7 +956,7 @@ class RemoteAddParamFixture(t.NamedTuple):
)
def test_remote_manager_add_params(
git_repo: GitSync,
create_git_remote_repo: CreateRepoPytestFixtureFn,
create_git_remote_repo: CreateRepoFn,
test_id: str,
fetch: bool | None,
track: str | None,
Expand Down Expand Up @@ -2287,7 +2287,7 @@ def test_reflog_entry_delete(git_repo: GitSync) -> None:
@pytest.fixture
def submodule_repo(
tmp_path: pathlib.Path,
git_commit_envvars: dict[str, str],
git_commit_envvars: GitCommitEnvVars,
set_gitconfig: pathlib.Path,
) -> git.Git:
"""Create a git repository to use as a submodule source."""
Expand Down
Loading
Loading