Skip to content

Commit 1c083d9

Browse files
Merge branch 'main' into copilot/add-prek-hook-forbid-double-backticks-again
2 parents 01ff0a8 + 6626b42 commit 1c083d9

33 files changed

Lines changed: 481 additions & 385 deletions

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ repos:
136136
always_run: true
137137
pass_filenames: false
138138
priority: 0
139+
- id: no-bare-managers-in-tests
140+
name: no-bare-managers-in-tests
141+
entry: '^\s*with\b.*Manager\('
142+
language: pygrep
143+
files: ^tests/
144+
exclude: ^tests/usethis/_file/(ini/test_ini_io_|toml/test_toml_io_|yaml/test_yaml_io_)\.py$
145+
types: [python]
146+
priority: 0
139147
- repo: https://github.com/astral-sh/ruff-pre-commit
140148
rev: v0.14.3
141149
hooks:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ dev = [
7575
"prek>=0.3.8",
7676
"pyinstrument>=5.1.1",
7777
"pyproject-fmt>=2.11.1",
78+
"rapidfuzz>=3.14.3,!=3.14.4",
7879
"ruff>=0.14.3",
7980
"ty>=0.0.1a25",
8081
"validate-pyproject>=0.24.1",

src/usethis/_config.py

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from contextlib import contextmanager
6-
from dataclasses import dataclass
6+
from dataclasses import dataclass, fields, replace
77
from pathlib import Path
88
from typing import TYPE_CHECKING, Literal
99

@@ -58,8 +58,12 @@ class UsethisConfig:
5858
subprocess_verbose: bool = False
5959
project_dir: Path | None = None
6060

61+
def copy(self) -> UsethisConfig:
62+
"""Return a shallow copy of this configuration."""
63+
return replace(self)
64+
6165
@contextmanager
62-
def set( # noqa: PLR0913, PLR0915
66+
def set( # noqa: PLR0913
6367
self,
6468
*,
6569
offline: bool | None = None,
@@ -74,24 +78,14 @@ def set( # noqa: PLR0913, PLR0915
7478
project_dir: Path | str | None = None,
7579
) -> Generator[None, None, None]:
7680
"""Temporarily change command options."""
77-
old_offline = self.offline
78-
old_quiet = self.quiet
79-
old_frozen = self.frozen
80-
old_alert_only = self.alert_only
81-
old_instruct_only = self.instruct_only
82-
old_backend = self.backend
83-
old_inferred_backend = self.inferred_backend
84-
old_build_backend = self.build_backend
85-
old_disable_pre_commit = self.disable_pre_commit
86-
old_subprocess_verbose = self.subprocess_verbose
87-
old_project_dir = self.project_dir
81+
old = self.copy()
8882

8983
if offline is None:
90-
offline = old_offline
84+
offline = self.offline
9185
if quiet is None:
92-
quiet = old_quiet
86+
quiet = self.quiet
9387
if frozen is None:
94-
frozen = old_frozen
88+
frozen = self.frozen
9589
if alert_only is None:
9690
alert_only = self.alert_only
9791
if instruct_only is None:
@@ -101,11 +95,11 @@ def set( # noqa: PLR0913, PLR0915
10195
if build_backend is None:
10296
build_backend = self.build_backend
10397
if disable_pre_commit is None:
104-
disable_pre_commit = old_disable_pre_commit
98+
disable_pre_commit = self.disable_pre_commit
10599
if subprocess_verbose is None:
106-
subprocess_verbose = old_subprocess_verbose
100+
subprocess_verbose = self.subprocess_verbose
107101
if project_dir is None:
108-
project_dir = old_project_dir
102+
project_dir = self.project_dir
109103

110104
self.offline = offline
111105
self.quiet = quiet
@@ -122,17 +116,12 @@ def set( # noqa: PLR0913, PLR0915
122116
project_dir = Path(project_dir)
123117
self.project_dir = project_dir
124118
yield
125-
self.offline = old_offline
126-
self.quiet = old_quiet
127-
self.frozen = old_frozen
128-
self.alert_only = old_alert_only
129-
self.instruct_only = old_instruct_only
130-
self.backend = old_backend
131-
self.inferred_backend = old_inferred_backend
132-
self.build_backend = old_build_backend
133-
self.disable_pre_commit = old_disable_pre_commit
134-
self.subprocess_verbose = old_subprocess_verbose
135-
self.project_dir = old_project_dir
119+
self._restore(old)
120+
121+
def _restore(self, other: UsethisConfig) -> None:
122+
"""Restore all attributes from another configuration instance."""
123+
for f in fields(self):
124+
setattr(self, f.name, getattr(other, f.name))
136125

137126
def cpd(self) -> Path:
138127
"""Return the current project directory."""

tests/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from usethis._backend.uv.call import call_uv_subprocess
1010
from usethis._config import usethis_config
11+
from usethis._config_file import files_manager
1112
from usethis._console import _cached_warn_print, get_icon_mode
1213
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
1314
from usethis._integrations.environ.python import _get_current_python_version
@@ -55,7 +56,8 @@ def _uv_init_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
5556
# Without this, uv is used for initializing the project directory, but there's
5657
# no real indication that it's being used anywhere! So tests would suggest
5758
# --how behaviour based on --backend=none logic.
58-
with PyprojectTOMLManager() as mgr:
59+
with files_manager():
60+
mgr = PyprojectTOMLManager()
5961
mgr[["tool", "uv", "environments"]] = []
6062

6163
return tmp_path

tests/test_install.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from usethis._backend.uv.call import call_uv_subprocess
99
from usethis._config import usethis_config
10-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
10+
from usethis._config_file import files_manager
1111
from usethis._subprocess import call_subprocess
1212
from usethis._test import change_cwd
1313

@@ -35,7 +35,7 @@ def usethis_installed_dir(
3535
call_subprocess(["git", "add", "."])
3636
call_subprocess(["git", "commit", "-m", "Initial commit"])
3737

38-
with change_cwd(uv_init_dir), PyprojectTOMLManager():
38+
with change_cwd(uv_init_dir), files_manager():
3939
# Install usethis in a virtual environment
4040
call_uv_subprocess(
4141
["add", f"{copy_usethis_dev_dir.as_posix()}"], change_toml=True

tests/usethis/_backend/uv/test_call.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from usethis._backend.uv.call import call_uv_subprocess
77
from usethis._backend.uv.errors import UVSubprocessFailedError
88
from usethis._config import usethis_config
9-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
9+
from usethis._config_file import files_manager
1010
from usethis._test import change_cwd
1111

1212

@@ -63,7 +63,7 @@ def test_handle_missing_version(
6363
)
6464

6565
# Act
66-
with change_cwd(tmp_path), PyprojectTOMLManager():
66+
with change_cwd(tmp_path), files_manager():
6767
call_uv_subprocess(["add", "ruff==0.9.0"], change_toml=True)
6868

6969
# Assert

tests/usethis/_backend/uv/test_init.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from usethis._backend.uv.init import opinionated_uv_init
44
from usethis._config import usethis_config
5+
from usethis._config_file import files_manager
56
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
67
from usethis._test import change_cwd
78
from usethis._types.build_backend import BuildBackendEnum
89

910

1011
class TestOpinionatedUVINit:
1112
def test_build_backend_is_hatch(self, tmp_path: Path):
12-
with change_cwd(tmp_path), PyprojectTOMLManager() as manager:
13+
with change_cwd(tmp_path), files_manager():
14+
manager = PyprojectTOMLManager()
1315
# Act
1416
opinionated_uv_init()
1517

@@ -19,9 +21,10 @@ def test_build_backend_is_hatch(self, tmp_path: Path):
1921
def test_build_backend_is_uv(self, tmp_path: Path):
2022
with (
2123
change_cwd(tmp_path),
22-
PyprojectTOMLManager() as manager,
24+
files_manager(),
2325
usethis_config.set(build_backend=BuildBackendEnum.uv),
2426
):
27+
manager = PyprojectTOMLManager()
2528
# Act
2629
opinionated_uv_init()
2730

tests/usethis/_backend/uv/test_link_mode.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from pathlib import Path
22

33
from usethis._backend.uv.link_mode import ensure_symlink_mode
4-
from usethis._backend.uv.toml import UVTOMLManager
5-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
4+
from usethis._config_file import files_manager
65
from usethis._test import change_cwd
76

87

98
class TestEnsureSymlinkMode:
109
def test_symlink_mode_set(self, tmp_path: Path):
1110
# Act
12-
with change_cwd(tmp_path), PyprojectTOMLManager():
11+
with change_cwd(tmp_path), files_manager():
1312
ensure_symlink_mode()
1413

1514
# Assert
@@ -28,7 +27,7 @@ def test_uv_lock_takes_precedence(self, tmp_path: Path):
2827
(tmp_path / "uv.toml").touch()
2928

3029
# Act
31-
with change_cwd(tmp_path), PyprojectTOMLManager(), UVTOMLManager():
30+
with change_cwd(tmp_path), files_manager():
3231
ensure_symlink_mode()
3332

3433
# Assert

tests/usethis/_backend/uv/test_python.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
get_available_uv_python_versions,
66
get_supported_uv_minor_python_versions,
77
)
8-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
8+
from usethis._config_file import files_manager
99
from usethis._python.version import PythonVersion
1010
from usethis._test import change_cwd
1111

@@ -30,7 +30,7 @@ def test_lower_bound(self, tmp_path: Path):
3030
)
3131

3232
# Act
33-
with change_cwd(tmp_path), PyprojectTOMLManager():
33+
with change_cwd(tmp_path), files_manager():
3434
supported_python = get_supported_uv_minor_python_versions()
3535

3636
# Assert
@@ -47,15 +47,15 @@ def test_upper_bound(self, tmp_path: Path):
4747
)
4848

4949
# Act
50-
with change_cwd(tmp_path), PyprojectTOMLManager():
50+
with change_cwd(tmp_path), files_manager():
5151
supported_python = get_supported_uv_minor_python_versions()
5252

5353
# Assert
5454
assert [v.minor for v in supported_python] == ["9", "10", "11"]
5555
assert all(v.patch is None for v in supported_python)
5656

5757
def test_no_pyproject_toml(self, tmp_path: Path):
58-
with change_cwd(tmp_path), PyprojectTOMLManager():
58+
with change_cwd(tmp_path), files_manager():
5959
result = get_supported_uv_minor_python_versions()
6060
current_version = PythonVersion.from_interpreter()
6161
assert len(result) == 1
@@ -75,7 +75,7 @@ def test_no_requires_python(self, tmp_path: Path):
7575
# Act
7676
with (
7777
change_cwd(tmp_path),
78-
PyprojectTOMLManager(),
78+
files_manager(),
7979
):
8080
versions = get_supported_uv_minor_python_versions()
8181

tests/usethis/_core/test_author.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from usethis._config_file import files_manager
56
from usethis._core.author import add_author
67
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
78
from usethis._file.toml.errors import (
@@ -17,7 +18,7 @@ def test_no_authors_yet(self, tmp_path: Path, capfd: pytest.CaptureFixture[str])
1718
(tmp_path / "pyproject.toml").touch()
1819

1920
# Act
20-
with change_cwd(tmp_path), PyprojectTOMLManager():
21+
with change_cwd(tmp_path), files_manager():
2122
add_author(
2223
name="John Cleese",
2324
email="jc@example.com",
@@ -48,7 +49,7 @@ def test_append(self, tmp_path: Path):
4849
)
4950

5051
# Act
51-
with change_cwd(tmp_path), PyprojectTOMLManager():
52+
with change_cwd(tmp_path), files_manager():
5253
add_author(
5354
name="John Cleese",
5455
email="jc@example.com",
@@ -71,7 +72,7 @@ def test_email_not_provided(self, tmp_path: Path):
7172
(tmp_path / "pyproject.toml").touch()
7273

7374
# Act
74-
with change_cwd(tmp_path), PyprojectTOMLManager():
75+
with change_cwd(tmp_path), files_manager():
7576
add_author(name="John Cleese")
7677

7778
# Assert
@@ -98,7 +99,7 @@ def test_overwrite(self, tmp_path: Path):
9899
)
99100

100101
# Act
101-
with change_cwd(tmp_path), PyprojectTOMLManager():
102+
with change_cwd(tmp_path), files_manager():
102103
add_author(
103104
name="Python Dev Team",
104105
overwrite=True,
@@ -116,7 +117,7 @@ def test_overwrite(self, tmp_path: Path):
116117

117118
def test_no_pyproject_yet(self, tmp_path: Path):
118119
# Act
119-
with change_cwd(tmp_path), PyprojectTOMLManager():
120+
with change_cwd(tmp_path), files_manager():
120121
add_author(name="John Cleese")
121122

122123
# Assert
@@ -138,12 +139,14 @@ def test_doesnt_break_other_sections(self, tmp_path: Path):
138139
)
139140

140141
# Act
141-
with change_cwd(tmp_path), PyprojectTOMLManager() as manager:
142+
with change_cwd(tmp_path), files_manager():
143+
manager = PyprojectTOMLManager()
142144
add_author(name="John Cleese")
143145
assert ["project", "scripts"] in manager
144146

145147
# Assert
146-
with change_cwd(tmp_path), PyprojectTOMLManager() as manager:
148+
with change_cwd(tmp_path), files_manager():
149+
manager = PyprojectTOMLManager()
147150
assert ["project"] in manager
148151
assert ["project", "scripts"] in manager
149152

@@ -158,7 +161,7 @@ def test_project_section_not_a_mapping(self, tmp_path: Path):
158161
# Act
159162
with (
160163
change_cwd(tmp_path),
161-
PyprojectTOMLManager(),
164+
files_manager(),
162165
pytest.raises(
163166
TOMLValueMissingError,
164167
match=r"'project' is not a valid mapping .* does not contain the key 'authors'",
@@ -178,7 +181,7 @@ def test_authors_section_not_a_list(self, tmp_path: Path):
178181
# Act
179182
with (
180183
change_cwd(tmp_path),
181-
PyprojectTOMLManager(),
184+
files_manager(),
182185
pytest.raises(
183186
TOMLValueInvalidError, match=r"'project.authors' is not a valid list"
184187
),

0 commit comments

Comments
 (0)