Skip to content

Commit 7d6bc81

Browse files
authored
pytest fixtures: hg / git config fixtures to session scope (#475)
_Resolves #474_ # Problem Issue #472 requires session-scoped fixtures. Currently, we use `set_home`, `gitconfig`, and `hgconfig` to `monkeypatch.setenv` `$HOME`, pointing it to a temporary directory with user-specific configuration files. This ensures all subsequent `git` and `hg` commands automatically load these configurations. However, [`monkeypatch.setenv`](https://docs.pytest.org/en/8.3.x/reference/reference.html#pytest.MonkeyPatch.setenv) doesn't work with function-scoped fixtures. # Improvement ``` ❯ hyperfine \ --warmup 3 \ --runs 10 \ --prepare 'git checkout master' \ --command-name 'libvcs 0.31.0' \ 'py.test' \ --prepare 'git checkout pytest-config' \ --command-name 'with improved hg/git config fixtures' \ 'py.test' Benchmark 1: libvcs 0.31.0 Time (mean ± σ): 15.150 s ± 0.751 s [User: 16.650 s, System: 4.720 s] Range (min … max): 14.235 s … 16.741 s 10 runs Benchmark 2: with improved hg/git config fixtures Time (mean ± σ): 15.014 s ± 0.307 s [User: 17.246 s, System: 4.865 s] Range (min … max): 14.458 s … 15.642 s 10 runs Summary with improved hg/git config fixtures ran 1.01 ± 0.05 times faster than libvcs 0.31.0 ``` # Changes ## pytest fixtures: Session-scoped `hgconfig` and `gitconfig` These are now set by `set_hgconfig` and `set_gitconfig`, which set `HGRCPATH` and `GIT_CONFIG`, instead of overriding `HOME`.
2 parents 58a9247 + 43eebcb commit 7d6bc81

File tree

7 files changed

+148
-86
lines changed

7 files changed

+148
-86
lines changed

CHANGES

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ $ pip install --user --upgrade --pre libvcs
1515

1616
<!-- Maintainers, insert changes / features for the next release here -->
1717

18+
## Breaking changes
19+
20+
### pytest fixtures: Session-scoped `hgconfig` and `gitconfig` (#475)
21+
22+
These are now set by `set_hgconfig` and `set_gitconfig`, which set `HGRCPATH` and `GIT_CONFIG`, instead of overriding `HOME`.
23+
24+
## Documentation
25+
26+
- Updates for pytest plugin documentation.
27+
1828
## libvcs 0.31.0 (2024-10-12)
1929

2030
### Breaking changes

docs/pytest-plugin.md

+57-33
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
(pytest_plugin)=
22

3-
# `pytest` plugin
3+
# `pytest` Plugin
44

5-
Create git, svn, and hg repos on the fly in [pytest].
5+
With libvcs's pytest plugin for [pytest], you can easily create Git, SVN, and Mercurial repositories on the fly.
66

7-
```{seealso} Using libvcs?
7+
```{seealso} Are you using libvcs?
88
9-
Do you want more flexibility? Correctness? Power? Defaults changed? [Connect with us] on the tracker, we want to know
10-
your case, we won't stabilize APIs until we're sure everything is by the book.
9+
Looking for more flexibility, correctness, or power? Need different defaults? [Connect with us] on GitHub. We'd love to hear about your use case—APIs won't be stabilized until we're confident everything meets expectations.
1110
1211
[connect with us]: https://github.com/vcs-python/libvcs/discussions
13-
1412
```
1513

1614
```{module} libvcs.pytest_plugin
@@ -21,66 +19,92 @@ your case, we won't stabilize APIs until we're sure everything is by the book.
2119

2220
## Usage
2321

24-
Install `libvcs` via the python package manager of your choosing, e.g.
22+
Install `libvcs` using your preferred Python package manager:
2523

2624
```console
2725
$ pip install libvcs
2826
```
2927

30-
The pytest plugin will automatically be detected via pytest, and the fixtures will be added.
28+
Pytest will automatically detect the plugin, and its fixtures will be available.
3129

3230
## Fixtures
3331

34-
`pytest-vcs` works through providing {ref}`pytest fixtures <pytest:fixtures-api>` - so read up on
35-
those!
32+
This pytest plugin works by providing {ref}`pytest fixtures <pytest:fixtures-api>`. The plugin's fixtures ensure that a fresh Git, Subversion, or Mercurial repository is available for each test. It utilizes [session-scoped fixtures] to cache initial repositories, improving performance across tests.
3633

37-
The plugin's fixtures guarantee a fresh git repository every test.
34+
[session-scoped fixtures]: https://docs.pytest.org/en/8.3.x/how-to/fixtures.html#fixture-scopes
3835

3936
(recommended-fixtures)=
4037

41-
## Recommended fixtures
38+
## Recommended Fixtures
4239

43-
These fixtures are automatically used when the plugin is enabled and `pytest` is run.
40+
When the plugin is enabled and `pytest` is run, these fixtures are automatically used:
4441

45-
- Creating temporary, test directories for:
42+
- Create temporary test directories for:
4643
- `/home/` ({func}`home_path`)
4744
- `/home/${user}` ({func}`user_path`)
48-
- Setting your home directory
49-
- Patch `$HOME` to point to {func}`user_path` ({func}`set_home`)
50-
- Set default configuration
45+
- Set the home directory:
46+
- Patch `$HOME` to point to {func}`user_path` using ({func}`set_home`)
47+
- Create configuration files:
48+
- `.gitconfig` via {func}`gitconfig`
49+
- `.hgrc` via {func}`hgconfig`
50+
- Set default VCS configurations:
51+
- Use {func}`hgconfig` for [`HGRCPATH`] via {func}`set_hgconfig`
52+
- Use {func}`gitconfig` for [`GIT_CONFIG`] via {func}`set_gitconfig`
53+
54+
These ensure that repositories can be cloned and created without unnecessary warnings.
55+
56+
[`HGRCPATH`]: https://www.mercurial-scm.org/doc/hg.1.html#:~:text=UNIX%2Dlike%20environments.-,HGRCPATH,-If%20not%20set
57+
[`GIT_CONFIG`]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIG
58+
59+
## Bootstrapping pytest in `conftest.py`
60+
61+
To configure the above fixtures with `autouse=True`, add them to your `conftest.py` file or test file, depending on the desired scope.
62+
63+
_Why aren't these fixtures added automatically by the plugin?_ This design choice promotes explicitness, adhering to best practices for pytest plugins and Python packages.
64+
65+
### Setting a Temporary Home Directory
66+
67+
To set a temporary home directory, use the {func}`set_home` fixture with `autouse=True`:
68+
69+
```python
70+
import pytest
71+
72+
@pytest.fixture(autouse=True)
73+
def setup(set_home: None):
74+
pass
75+
```
5176

52-
- `.gitconfig`, via {func}`gitconfig`:
53-
- `.hgrc`, via {func}`hgconfig`:
77+
### Setting a Default VCS Configuration
5478

55-
These are set to ensure you can correctly clone and create repositories without without extra
56-
warnings.
79+
#### Git
5780

58-
## Bootstrapping pytest in your `conftest.py`
81+
Use the {func}`set_gitconfig` fixture with `autouse=True`:
5982

60-
The most common scenario is you will want to configure the above fixtures with `autouse`.
83+
```python
84+
import pytest
6185

62-
_Why doesn't the plugin automatically add them?_ It's part of being a decent pytest plugin and
63-
python package: explicitness.
86+
@pytest.fixture(autouse=True)
87+
def setup(set_gitconfig: None):
88+
pass
89+
```
6490

65-
(set_home)=
91+
#### Mercurial
6692

67-
### Setting a temporary home directory
93+
Use the {func}`set_hgconfig` fixture with `autouse=True`:
6894

6995
```python
7096
import pytest
7197

7298
@pytest.fixture(autouse=True)
73-
def setup(
74-
set_home: None,
75-
):
99+
def setup(set_hgconfig: None):
76100
pass
77101
```
78102

79-
## See examples
103+
## Examples
80104

81-
View libvcs's own [tests/](https://github.com/vcs-python/libvcs/tree/master/tests)
105+
For usage examples, refer to libvcs's own [tests/](https://github.com/vcs-python/libvcs/tree/master/tests).
82106

83-
## API reference
107+
## API Reference
84108

85109
```{eval-rst}
86110
.. automodule:: libvcs.pytest_plugin

src/libvcs/pytest_plugin.py

+39-25
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,25 @@ def set_home(
108108
monkeypatch.setenv("HOME", str(user_path))
109109

110110

111-
@pytest.fixture
111+
vcs_email = "[email protected]"
112+
113+
114+
@pytest.fixture(scope="session")
112115
@skip_if_git_missing
113-
def gitconfig(user_path: pathlib.Path, set_home: pathlib.Path) -> pathlib.Path:
116+
def gitconfig(
117+
user_path: pathlib.Path,
118+
) -> pathlib.Path:
114119
"""Return git configuration, pytest fixture."""
115120
gitconfig = user_path / ".gitconfig"
116-
user_email = "[email protected]"
121+
122+
if gitconfig.exists():
123+
return gitconfig
124+
117125
gitconfig.write_text(
118126
textwrap.dedent(
119127
f"""
120128
[user]
121-
email = {user_email}
129+
email = {vcs_email}
122130
name = {getpass.getuser()}
123131
[color]
124132
diff = auto
@@ -127,26 +135,26 @@ def gitconfig(user_path: pathlib.Path, set_home: pathlib.Path) -> pathlib.Path:
127135
encoding="utf-8",
128136
)
129137

130-
output = run(["git", "config", "--get", "user.email"])
131-
used_config_file_output = run(
132-
[
133-
"git",
134-
"config",
135-
"--show-origin",
136-
"--get",
137-
"user.email",
138-
],
139-
)
140-
assert str(gitconfig) in used_config_file_output
141-
assert user_email in output, "Should use our fixture config and home directory"
142-
143138
return gitconfig
144139

145140

146141
@pytest.fixture
142+
@skip_if_git_missing
143+
def set_gitconfig(
144+
monkeypatch: pytest.MonkeyPatch,
145+
gitconfig: pathlib.Path,
146+
) -> pathlib.Path:
147+
"""Set git configuration."""
148+
monkeypatch.setenv("GIT_CONFIG", str(gitconfig))
149+
return gitconfig
150+
151+
152+
@pytest.fixture(scope="session")
147153
@skip_if_hg_missing
148-
def hgconfig(user_path: pathlib.Path, set_home: pathlib.Path) -> pathlib.Path:
149-
"""Return Mercurial configuration, pytest fixture."""
154+
def hgconfig(
155+
user_path: pathlib.Path,
156+
) -> pathlib.Path:
157+
"""Return Mercurial configuration."""
150158
hgrc = user_path / ".hgrc"
151159
hgrc.write_text(
152160
textwrap.dedent(
@@ -164,6 +172,17 @@ def hgconfig(user_path: pathlib.Path, set_home: pathlib.Path) -> pathlib.Path:
164172
return hgrc
165173

166174

175+
@pytest.fixture
176+
@skip_if_hg_missing
177+
def set_hgconfig(
178+
monkeypatch: pytest.MonkeyPatch,
179+
hgconfig: pathlib.Path,
180+
) -> pathlib.Path:
181+
"""Set Mercurial configuration."""
182+
monkeypatch.setenv("HGRCPATH", str(hgconfig))
183+
return hgconfig
184+
185+
167186
@pytest.fixture
168187
def projects_path(
169188
user_path: pathlib.Path,
@@ -490,8 +509,7 @@ def svn_remote_repo(
490509
create_svn_remote_repo: CreateRepoPytestFixtureFn,
491510
) -> pathlib.Path:
492511
"""Pre-made. Local file:// based SVN server."""
493-
repo_path = create_svn_remote_repo()
494-
return repo_path
512+
return create_svn_remote_repo()
495513

496514

497515
@pytest.fixture(scope="session")
@@ -690,8 +708,6 @@ def add_doctest_fixtures(
690708
doctest_namespace: dict[str, Any],
691709
tmp_path: pathlib.Path,
692710
set_home: pathlib.Path,
693-
gitconfig: pathlib.Path,
694-
hgconfig: pathlib.Path,
695711
create_git_remote_repo: CreateRepoPytestFixtureFn,
696712
create_svn_remote_repo: CreateRepoPytestFixtureFn,
697713
create_hg_remote_repo: CreateRepoPytestFixtureFn,
@@ -704,7 +720,6 @@ def add_doctest_fixtures(
704720
return
705721
doctest_namespace["tmp_path"] = tmp_path
706722
if shutil.which("git"):
707-
doctest_namespace["gitconfig"] = gitconfig
708723
doctest_namespace["create_git_remote_repo"] = functools.partial(
709724
create_git_remote_repo,
710725
remote_repo_post_init=git_remote_repo_single_commit_post_init,
@@ -719,7 +734,6 @@ def add_doctest_fixtures(
719734
remote_repo_post_init=svn_remote_repo_single_commit_post_init,
720735
)
721736
if shutil.which("hg"):
722-
doctest_namespace["hgconfig"] = hgconfig
723737
doctest_namespace["create_hg_remote_repo_bare"] = create_hg_remote_repo
724738
doctest_namespace["create_hg_remote_repo"] = functools.partial(
725739
create_hg_remote_repo,

tests/sync/test_hg.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,20 @@
88
from libvcs import exc
99
from libvcs._internal.run import run
1010
from libvcs._internal.shortcuts import create_project
11-
from libvcs.pytest_plugin import CreateRepoPytestFixtureFn
1211
from libvcs.sync.hg import HgSync
1312

1413
if not shutil.which("hg"):
1514
pytestmark = pytest.mark.skip(reason="hg is not available")
1615

1716

18-
@pytest.fixture
19-
def hg_remote_repo(
20-
set_home: pathlib.Path,
21-
hgconfig: pathlib.Path,
22-
create_hg_remote_repo: CreateRepoPytestFixtureFn,
17+
@pytest.fixture(autouse=True)
18+
def set_hgconfig(
19+
set_hgconfig: pathlib.Path,
2320
) -> pathlib.Path:
24-
"""Create a remote hg repository."""
25-
return create_hg_remote_repo()
21+
"""Set mercurial configuration."""
22+
return set_hgconfig
2623

2724

28-
@pytest.mark.usefixtures("set_home", "hgconfig")
2925
def test_hg_sync(
3026
tmp_path: pathlib.Path,
3127
projects_path: pathlib.Path,

tests/sync/test_svn.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def test_svn_sync(tmp_path: pathlib.Path, svn_remote_repo: pathlib.Path) -> None
3131

3232

3333
def test_svn_sync_with_files(
34-
tmp_path: pathlib.Path, svn_remote_repo_with_files: pathlib.Path
34+
tmp_path: pathlib.Path,
35+
svn_remote_repo_with_files: pathlib.Path,
3536
) -> None:
3637
"""Tests for SvnSync."""
3738
repo_name = "my_svn_project"

tests/test_pytest_plugin.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import pytest
88

9-
from libvcs.pytest_plugin import CreateRepoPytestFixtureFn
9+
from libvcs._internal.run import run
10+
from libvcs.pytest_plugin import CreateRepoPytestFixtureFn, vcs_email
1011

1112

1213
@pytest.mark.skipif(not shutil.which("git"), reason="git is not available")
@@ -109,3 +110,22 @@ def test_repo_git_remote_checkout(
109110
# Test
110111
result = pytester.runpytest(str(first_test_filename))
111112
result.assert_outcomes(passed=1)
113+
114+
115+
def test_gitconfig(
116+
gitconfig: pathlib.Path,
117+
set_gitconfig: pathlib.Path,
118+
) -> None:
119+
"""Test gitconfig fixture."""
120+
output = run(["git", "config", "--get", "user.email"])
121+
used_config_file_output = run(
122+
[
123+
"git",
124+
"config",
125+
"--show-origin",
126+
"--get",
127+
"user.email",
128+
],
129+
)
130+
assert str(gitconfig) in used_config_file_output
131+
assert vcs_email in output, "Should use our fixture config and home directory"

0 commit comments

Comments
 (0)