Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1b4021d
Only use `pyproject.toml` for config when it already exists
nathanjmcdougall Aug 17, 2025
088b2a3
Merge branch 'main' into 913-fallback-to-rufftoml-as-the-preferred-co…
nathanjmcdougall Sep 14, 2025
9033178
update token in README test
nathanjmcdougall Sep 14, 2025
31408bc
Remove empty test module
nathanjmcdougall Sep 14, 2025
bce27a3
Split test to reflect new codespell config file behaviour
nathanjmcdougall Sep 14, 2025
59d9ba7
Consistently name tests `*_no_pyproject_toml` rather than `*_no_pypro…
nathanjmcdougall Sep 14, 2025
8450f5f
Split docstyle no backend test into two based on `pyproject.toml` pre…
nathanjmcdougall Sep 14, 2025
5773054
Only add `pyproject-fmt` if `pyproject.toml` is actually used in the …
nathanjmcdougall Sep 14, 2025
0dafb8c
Fix docstyle `test_none_backend_no_pyproject_toml` test
nathanjmcdougall Sep 14, 2025
0b6daba
Split the `test_non_backend` test for `usethis lint` into two based o…
nathanjmcdougall Sep 14, 2025
b327b8f
Split the `test_non_backend` test for `usethis rule` into two based o…
nathanjmcdougall Sep 14, 2025
a247a95
Split the `test_non_backend` test for `usethis spellcheck` into two b…
nathanjmcdougall Sep 14, 2025
c8e3f9a
Split the `test_non_backend` test for `usethis test` into two based o…
nathanjmcdougall Sep 14, 2025
5c33e08
Split the `test_non_backend` test for `usethis tool coverage.py` into…
nathanjmcdougall Sep 14, 2025
af7a1ab
Don't create empty config files (for Deptry etc.) unnecessarily
nathanjmcdougall Sep 14, 2025
50fd81f
Merge branch 'main' into 913-fallback-to-rufftoml-as-the-preferred-co…
nathanjmcdougall Sep 14, 2025
d4f3777
Pass `test_empty_dir` test for Ruff
nathanjmcdougall Sep 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
if TYPE_CHECKING:
from pathlib import Path

from usethis._integrations.backend.uv.deps import (
Dependency,
)
from usethis._integrations.backend.uv.deps import Dependency
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
from usethis._integrations.pre_commit.schema import LocalRepo, UriRepo
from usethis._io import KeyValueFileManager
Expand Down Expand Up @@ -409,7 +407,11 @@ def add_configs(self) -> None:
def _add_config_item(
self, config_item: ConfigItem, *, file_managers: set[KeyValueFileManager]
) -> bool:
"""Add a specific configuration item using specified file managers."""
"""Add a specific configuration item using specified file managers.

Returns whether any config was added. Config might not be added in some cases
where it's conditional and not applicable based on the current project state.
"""
# This is mostly a helper method for `add_configs`.

# Filter to just those active config file managers which can manage this
Expand All @@ -425,15 +427,10 @@ def _add_config_item(
msg = f"No active config file managers found for one of the '{self.name}' config items."
raise NotImplementedError(msg)
else:
# Early exist; this config item is not managed by any active files
# Early exit; this config item is not managed by any active files
# so it's optional, effectively.
return False

for file_manager in used_file_managers:
if not (file_manager.path.exists() and file_manager.path.is_file()):
tick_print(f"Writing '{file_manager.relative_path}'.")
file_manager.path.touch(exist_ok=True)

config_entries = [
config_item
for relative_path, config_item in config_item.root.items()
Expand All @@ -456,6 +453,13 @@ def _add_config_item(
# No value to add, so skip this config item.
return False

# N.B. we wait to create files until after all `return False` lines to avoid
# creating empty files unnecessarily.
for file_manager in used_file_managers:
if not (file_manager.path.exists() and file_manager.path.is_file()):
tick_print(f"Writing '{file_manager.relative_path}'.")
file_manager.path.touch(exist_ok=True)

shared_keys = []
for key in entry.keys:
shared_keys.append(key)
Expand Down
10 changes: 10 additions & 0 deletions src/usethis/_tool/impl/codespell.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from typing_extensions import assert_never

from usethis._config import usethis_config
from usethis._config_file import CodespellRCManager
from usethis._console import box_print
from usethis._integrations.backend.dispatch import get_backend
Expand All @@ -22,6 +24,9 @@
from usethis._types.backend import BackendEnum
from usethis._types.deps import Dependency

if TYPE_CHECKING:
from usethis._io import KeyValueFileManager


class CodespellTool(Tool):
# https://github.com/codespell-project/codespell
Expand Down Expand Up @@ -54,6 +59,11 @@ def print_how_to_use(self) -> None:
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
return [Dependency(name="codespell")]

def preferred_file_manager(self) -> KeyValueFileManager:
if (usethis_config.cpd() / "pyproject.toml").exists():
return PyprojectTOMLManager()
return CodespellRCManager()

def get_config_spec(self) -> ConfigSpec:
# https://github.com/codespell-project/codespell?tab=readme-ov-file#using-a-config-file

Expand Down
10 changes: 10 additions & 0 deletions src/usethis/_tool/impl/coverage_py.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from usethis._config import usethis_config
from usethis._config_file import (
CoverageRCManager,
ToxINIManager,
Expand All @@ -17,6 +19,9 @@
from usethis._types.backend import BackendEnum
from usethis._types.deps import Dependency

if TYPE_CHECKING:
from usethis._io import KeyValueFileManager


class CoveragePyTool(Tool):
# https://github.com/nedbat/coveragepy
Expand Down Expand Up @@ -54,6 +59,11 @@ def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
deps += [Dependency(name="pytest-cov")]
return deps

def preferred_file_manager(self) -> KeyValueFileManager:
if (usethis_config.cpd() / "pyproject.toml").exists():
return PyprojectTOMLManager()
return CoverageRCManager()

def get_config_spec(self) -> ConfigSpec:
# https://coverage.readthedocs.io/en/latest/config.html#configuration-reference

Expand Down
5 changes: 5 additions & 0 deletions src/usethis/_tool/impl/import_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
# This is because it needs to run from within the virtual environment.
return [Dependency(name="import-linter")]

def preferred_file_manager(self) -> KeyValueFileManager:
if (usethis_config.cpd() / "pyproject.toml").exists():
return PyprojectTOMLManager()
return DotImportLinterManager()

def get_config_spec(self) -> ConfigSpec:
# https://import-linter.readthedocs.io/en/stable/usage.html

Expand Down
5 changes: 5 additions & 0 deletions src/usethis/_tool/impl/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
deps += [Dependency(name="pytest-cov")]
return deps

def preferred_file_manager(self) -> KeyValueFileManager:
if (usethis_config.cpd() / "pyproject.toml").exists():
return PyprojectTOMLManager()
return PytestINIManager()

def get_config_spec(self) -> ConfigSpec:
# https://docs.pytest.org/en/stable/reference/customize.html#configuration-file-formats
# "Options from multiple configfiles candidates are never merged - the first match wins."
Expand Down
5 changes: 5 additions & 0 deletions src/usethis/_tool/impl/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ def print_how_to_use_formatter(self) -> None:
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
return [Dependency(name="ruff")]

def preferred_file_manager(self) -> KeyValueFileManager:
if (usethis_config.cpd() / "pyproject.toml").exists():
return PyprojectTOMLManager()
return RuffTOMLManager()

def get_config_spec(self) -> ConfigSpec:
# https://docs.astral.sh/ruff/configuration/#config-file-discovery

Expand Down
4 changes: 3 additions & 1 deletion src/usethis/_toolset/format_.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from usethis._core.tool import use_pyproject_fmt, use_ruff
from usethis._tool.impl.pyproject_toml import PyprojectTOMLTool


def use_formatters(remove: bool = False, how: bool = False):
use_ruff(linter=False, formatter=True, remove=remove, how=how)
use_pyproject_fmt(remove=remove, how=how)
if PyprojectTOMLTool().is_used() and (not remove or how):
use_pyproject_fmt(remove=remove, how=how)
35 changes: 31 additions & 4 deletions tests/usethis/_core/test_core_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,17 @@ def test_setup_cfg_empty(self, uv_init_dir: Path):
assert (uv_init_dir / "setup.cfg").read_text() == ""
assert "[tool.codespell]" in (uv_init_dir / "pyproject.toml").read_text()

def test_none_backend(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
def test_none_backend_pyproject_toml(
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
):
# Arrange
(tmp_path / "pyproject.toml").touch()

# Act
with (
change_cwd(tmp_path),
usethis_config.set(backend=BackendEnum.none),
PyprojectTOMLManager(),
files_manager(),
):
use_codespell()

Expand All @@ -250,11 +255,31 @@ def test_none_backend(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
assert not err
assert out == (
"☐ Add the dev dependency 'codespell'.\n"
"✔ Writing 'pyproject.toml'.\n"
"✔ Adding Codespell config to 'pyproject.toml'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
)

def test_none_backend_no_pyproject_toml(
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
):
# Act
with (
change_cwd(tmp_path),
usethis_config.set(backend=BackendEnum.none),
files_manager(),
):
use_codespell()

# Assert
out, err = capfd.readouterr()
assert not err
assert out == (
"☐ Add the dev dependency 'codespell'.\n"
"✔ Writing '.codespellrc'.\n"
"✔ Adding Codespell config to '.codespellrc'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
)

class TestRemove:
@pytest.mark.usefixtures("_vary_network_conn")
def test_config_file(
Expand Down Expand Up @@ -2318,7 +2343,9 @@ def test_doesnt_invoke_ensure_dep_declaration_file(self, tmp_path: Path):
class TestPytest:
class TestAdd:
@pytest.mark.usefixtures("_vary_network_conn")
def test_no_pyproject(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
def test_no_pyproject_toml(
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
):
with change_cwd(tmp_path), files_manager():
# Act
use_pytest()
Expand Down
2 changes: 1 addition & 1 deletion tests/usethis/_integrations/backend/uv/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_upper_bound(self, tmp_path: Path):
# Assert
assert supported_major_python == [9, 10, 11]

def test_no_pyproject(self, tmp_path: Path):
def test_no_pyproject_toml(self, tmp_path: Path):
with change_cwd(tmp_path), PyprojectTOMLManager():
assert get_supported_uv_major_python_versions() == [
extract_major_version(get_python_version())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_lower_bound(self, tmp_path: Path):
# Assert
assert requires_python == SpecifierSet(">=3.7")

def test_no_pyproject(self, tmp_path: Path):
def test_no_pyproject_toml(self, tmp_path: Path):
with (
change_cwd(tmp_path),
PyprojectTOMLManager(),
Expand Down
26 changes: 26 additions & 0 deletions tests/usethis/_tool/impl/test_codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,29 @@ def test_latest_version(self):
"Failed to fetch GitHub tags (connection issues); skipping test"
)
raise err

class TestAddConfig:
def test_empty_dir(self, tmp_path: Path):
# Expect ruff.toml to be preferred

# Act
with change_cwd(tmp_path), files_manager():
CodespellTool().add_configs()

# Assert
assert (tmp_path / ".codespellrc").exists()
assert not (tmp_path / "setup.cfg").exists()
assert not (tmp_path / "pyproject.toml").exists()

def test_pyproject_toml_exists(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").touch()

# Act
with change_cwd(tmp_path), files_manager():
CodespellTool().add_configs()

# Assert
assert not (tmp_path / ".codespellrc").exists()
assert not (tmp_path / "setup.cfg").exists()
assert (tmp_path / "pyproject.toml").exists()
24 changes: 24 additions & 0 deletions tests/usethis/_tool/impl/test_coverage_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,27 @@ def test_after_codespell(self, tmp_path: Path):
with change_cwd(tmp_path), files_manager():
assert ["tool", "coverage"] in PyprojectTOMLManager()
assert "[tool.coverage]" in (tmp_path / "pyproject.toml").read_text()

class TestAddConfig:
def test_empty_dir(self, tmp_path: Path):
# Expect .coveragerc to be preferred

# Act
with change_cwd(tmp_path), files_manager():
CoveragePyTool().add_configs()

# Assert
assert (tmp_path / ".coveragerc").exists()
assert not (tmp_path / "pyproject.toml").exists()

def test_pyproject_toml_exists(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").touch()

# Act
with change_cwd(tmp_path), files_manager():
CoveragePyTool().add_configs()

# Assert
assert not (tmp_path / ".coveragerc").exists()
assert (tmp_path / "pyproject.toml").exists()
21 changes: 21 additions & 0 deletions tests/usethis/_tool/impl/test_deptry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from usethis._config_file import files_manager
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._test import change_cwd
from usethis._tool.config import ConfigEntry, ConfigItem
Expand Down Expand Up @@ -247,3 +248,23 @@ def test_with_rule(self, tmp_path: Path):

# Assert
assert result == ["DEP003"]

class TestAddConfig:
def test_empty_dir(self, tmp_path: Path):
# Act
with change_cwd(tmp_path), files_manager():
DeptryTool().add_configs()

# Assert
assert not (tmp_path / "pyproject.toml").exists()

def test_pyproject_toml_exists(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").touch()

# Act
with change_cwd(tmp_path), files_manager():
DeptryTool().add_configs()

# Assert
assert (tmp_path / "pyproject.toml").exists()
24 changes: 24 additions & 0 deletions tests/usethis/_tool/impl/test_import_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ def test_ruff_is_used_without_inp_rules(
"☐ Run 'lint-imports' to run Import Linter.\n"
)

class TestAddConfig:
def test_empty_dir(self, tmp_path: Path):
# Expect .importlinter to be preferred

# Act
with change_cwd(tmp_path), files_manager():
ImportLinterTool().add_configs()

# Assert
assert (tmp_path / ".importlinter").exists()
assert not (tmp_path / "pyproject.toml").exists()

def test_pyproject_toml_exists(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").touch()

# Act
with change_cwd(tmp_path), files_manager():
ImportLinterTool().add_configs()

# Assert
assert not (tmp_path / ".importlinter").exists()
assert (tmp_path / "pyproject.toml").exists()


class TestIsINPRule:
def test_inp_rule(self):
Expand Down
24 changes: 24 additions & 0 deletions tests/usethis/_tool/impl/test_pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,27 @@ def test_one_step(self, uv_init_dir: Path):
- echo 'Hello, world!'
"""
)

class TestAddConfig:
def test_empty_dir(self, tmp_path: Path):
# Expect pytest.ini to be preferred

# Act
with change_cwd(tmp_path), files_manager():
PytestTool().add_configs()

# Assert
assert (tmp_path / "pytest.ini").exists()
assert not (tmp_path / "pyproject.toml").exists()

def test_pyproject_toml_exists(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").touch()

# Act
with change_cwd(tmp_path), files_manager():
PytestTool().add_configs()

# Assert
assert not (tmp_path / "pytest.ini").exists()
assert (tmp_path / "pyproject.toml").exists()
Loading
Loading