Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/usethis/_integrations/backend/uv/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_supported_uv_minor_python_versions() -> list[PythonVersion]:
major=v.major, minor=v.minor, patch=None
)

return sorted(minor_versions.values(), key=lambda v: (int(v.major), int(v.minor)))
return sorted(minor_versions.values(), key=lambda v: v.to_short_tuple())


def _parse_python_version_from_uv_output(version: str) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_required_minor_python_versions() -> list[PythonVersion]:
versions_by_minor: dict[tuple[int, int], set[int]] = {}
for spec in requires_python:
parsed = PythonVersion.from_string(spec.version)
major_minor = (int(parsed.major), int(parsed.minor))
major_minor = parsed.to_short_tuple()
patch = int(parsed.patch) if parsed.patch else 0
versions_by_minor.setdefault(major_minor, set()).add(patch)

Expand Down
4 changes: 4 additions & 0 deletions src/usethis/_integrations/python/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def to_short_string(self) -> str:
"""Return X.Y format (e.g., '3.10')."""
return f"{self.major}.{self.minor}"

def to_short_tuple(self) -> tuple[int, int]:
"""Return (major, minor) as integers."""
return (int(self.major), int(self.minor))

def __str__(self) -> str:
"""Return full version string."""
if self.patch is None:
Expand Down
24 changes: 23 additions & 1 deletion src/usethis/_tool/impl/codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
from usethis._console import how_print
from usethis._integrations.backend.dispatch import get_backend
from usethis._integrations.backend.uv.used import is_uv_used
from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLNotFoundError
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.file.pyproject_toml.requires_python import (
MissingRequiresPythonError,
get_required_minor_python_versions,
)
from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager
from usethis._integrations.pre_commit import schema as pre_commit_schema
from usethis._integrations.python.version import PythonVersion
from usethis._tool.base import Tool
from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec
from usethis._tool.pre_commit import PreCommitConfig
Expand Down Expand Up @@ -61,7 +67,23 @@ def print_how_to_use(self) -> None:
assert_never(install_method)

def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
return [Dependency(name="codespell")]
deps = [Dependency(name="codespell")]

# Python < 3.11 needs tomli (instead of the stdlib tomllib) to read
# pyproject.toml files
if unconditional:
needs_tomli = True
else:
try:
versions = get_required_minor_python_versions()
except (MissingRequiresPythonError, PyprojectTOMLNotFoundError):
versions = [PythonVersion.from_interpreter()]

needs_tomli = any(v.to_short_tuple() < (3, 11) for v in versions)
if needs_tomli:
deps.append(Dependency(name="tomli"))

return deps

def preferred_file_manager(self) -> KeyValueFileManager:
if (usethis_config.cpd() / "pyproject.toml").exists():
Expand Down
23 changes: 19 additions & 4 deletions tests/usethis/_core/test_core_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,15 @@ def test_none_backend_pyproject_toml(
# Assert
out, err = capfd.readouterr()
assert not err
# Check if tomli is needed based on current interpreter
current_version = PythonVersion.from_interpreter()
needs_tomli = current_version.to_short_tuple() < (3, 11)
if needs_tomli:
expected_deps = "☐ Add the dev dependencies 'codespell', 'tomli'.\n"
else:
expected_deps = "☐ Add the dev dependency 'codespell'.\n"
assert out == (
"☐ Add the dev dependency 'codespell'.\n"
"✔ Adding Codespell config to 'pyproject.toml'.\n"
expected_deps + "✔ Adding Codespell config to 'pyproject.toml'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
)

Expand All @@ -305,9 +311,18 @@ def test_none_backend_no_pyproject_toml(
# Assert
out, err = capfd.readouterr()
assert not err
# Check if tomli is needed based on current interpreter
current_version = PythonVersion.from_interpreter()
needs_tomli = (int(current_version.major), int(current_version.minor)) < (
3,
11,
)
if needs_tomli:
expected_deps = "☐ Add the dev dependencies 'codespell', 'tomli'.\n"
else:
expected_deps = "☐ Add the dev dependency 'codespell'.\n"
assert out == (
"☐ Add the dev dependency 'codespell'.\n"
"✔ Writing '.codespellrc'.\n"
expected_deps + "✔ Writing '.codespellrc'.\n"
"✔ Adding Codespell config to '.codespellrc'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
)
Expand Down
115 changes: 115 additions & 0 deletions tests/usethis/_tool/impl/test_codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from usethis._integrations.ci.github.errors import GitHubTagError
from usethis._integrations.ci.github.tags import get_github_latest_tag
from usethis._integrations.pre_commit import schema
from usethis._integrations.python.version import PythonVersion
from usethis._test import change_cwd
from usethis._tool.impl.codespell import CodespellTool
from usethis._types.backend import BackendEnum
from usethis._types.deps import Dependency


class TestCodespellTool:
Expand Down Expand Up @@ -184,3 +186,116 @@ def test_pyproject_toml_exists(self, tmp_path: Path):
assert not (tmp_path / ".codespellrc").exists()
assert not (tmp_path / "setup.cfg").exists()
assert (tmp_path / "pyproject.toml").exists()

class TestGetDevDeps:
def test_requires_python_includes_3_10(self, tmp_path: Path):
# Arrange
# Create a pyproject.toml with requires-python that includes Python 3.10
(tmp_path / "pyproject.toml").write_text(
"""\
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.10"
"""
)

# Act
with change_cwd(tmp_path), files_manager():
deps = CodespellTool().get_dev_deps()

# Assert
assert Dependency(name="codespell") in deps
assert Dependency(name="tomli") in deps

def test_requires_python_only_3_11_and_above(self, tmp_path: Path):
# Arrange
# Create a pyproject.toml with requires-python that only includes Python >= 3.11
(tmp_path / "pyproject.toml").write_text(
"""\
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.11"
"""
)

# Act
with change_cwd(tmp_path), files_manager():
deps = CodespellTool().get_dev_deps()

# Assert
assert Dependency(name="codespell") in deps
assert Dependency(name="tomli") not in deps

def test_requires_python_range_includes_3_10(self, tmp_path: Path):
# Arrange
# Create a pyproject.toml with requires-python range that includes 3.10
(tmp_path / "pyproject.toml").write_text(
"""\
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.10,<3.13"
"""
)

# Act
with change_cwd(tmp_path), files_manager():
deps = CodespellTool().get_dev_deps()

# Assert
assert Dependency(name="codespell") in deps
assert Dependency(name="tomli") in deps

def test_no_pyproject_toml_3pt10(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
# Arrange - no pyproject.toml file, using interpreter version
monkeypatch.setattr(
"usethis._integrations.python.version.PythonVersion.from_interpreter",
lambda: PythonVersion(major="3", minor="10", patch=None),
)
# Act
with change_cwd(tmp_path), files_manager():
deps = CodespellTool().get_dev_deps()

# Assert
assert Dependency(name="codespell") in deps
assert Dependency(name="tomli") in deps

def test_no_pyproject_toml_3pt11(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
# Arrange - no pyproject.toml file, using interpreter version
monkeypatch.setattr(
"usethis._integrations.python.version.PythonVersion.from_interpreter",
lambda: PythonVersion(major="3", minor="11", patch=None),
)
# Act
with change_cwd(tmp_path), files_manager():
deps = CodespellTool().get_dev_deps()

# Assert
assert Dependency(name="codespell") in deps
assert Dependency(name="tomli") not in deps

def test_unconditional_includes_tomli(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").write_text(
"""\
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.12"
"""
)

# Act
with change_cwd(tmp_path), files_manager():
deps = CodespellTool().get_dev_deps(unconditional=True)

# Assert
# When unconditional=True, tomli should always be included
assert Dependency(name="codespell") in deps
assert Dependency(name="tomli") in deps
12 changes: 10 additions & 2 deletions tests/usethis/_ui/interface/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from usethis._config import usethis_config
from usethis._integrations.pre_commit.hooks import get_hook_ids
from usethis._integrations.pre_commit.yaml import PreCommitConfigYAMLManager
from usethis._integrations.python.version import PythonVersion
from usethis._test import CliRunner, change_cwd
from usethis._ui.app import app

Expand Down Expand Up @@ -142,6 +143,13 @@ def test_none_backend(self, tmp_path: Path):
# Assert
assert result.exit_code == 0, result.output
assert (tmp_path / "pyproject.toml").exists()
# Check if tomli is needed based on current interpreter
current_version = PythonVersion.from_interpreter()
needs_tomli = current_version.to_short_tuple() < (3, 11)
if needs_tomli:
expected_spellcheck = "☐ Add the dev dependencies 'codespell', 'tomli'.\n"
else:
expected_spellcheck = "☐ Add the dev dependency 'codespell'.\n"
assert result.output == (
"✔ Writing 'pyproject.toml' and initializing project.\n"
"✔ Writing 'README.md'.\n"
Expand All @@ -162,8 +170,8 @@ def test_none_backend(self, tmp_path: Path):
"☐ Run 'ruff format' to run the Ruff formatter.\n"
"☐ Run 'pyproject-fmt pyproject.toml' to run pyproject-fmt.\n"
"✔ Adding recommended spellcheckers.\n"
"☐ Add the dev dependency 'codespell'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
+ expected_spellcheck
+ "☐ Run 'codespell' to run the Codespell spellchecker.\n"
"✔ Adding recommended test frameworks.\n"
"☐ Add the test dependency 'pytest'.\n"
"☐ Add the test dependencies 'coverage', 'pytest-cov'.\n"
Expand Down
21 changes: 17 additions & 4 deletions tests/usethis/_ui/interface/test_spellcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from usethis._config_file import files_manager
from usethis._deps import get_deps_from_group
from usethis._integrations.python.version import PythonVersion
from usethis._test import CliRunner, change_cwd
from usethis._types.deps import Dependency
from usethis._ui.app import app
Expand Down Expand Up @@ -55,9 +56,15 @@ def test_none_backend(self, tmp_path: Path):
# Assert
assert result.exit_code == 0, result.output
assert not (tmp_path / "pyproject.toml").exists()
# Check if tomli is needed based on current interpreter
current_version = PythonVersion.from_interpreter()
needs_tomli = current_version.to_short_tuple() < (3, 11)
if needs_tomli:
expected_deps = "☐ Add the dev dependencies 'codespell', 'tomli'.\n"
else:
expected_deps = "☐ Add the dev dependency 'codespell'.\n"
assert result.output == (
"☐ Add the dev dependency 'codespell'.\n"
"✔ Writing '.codespellrc'.\n"
expected_deps + "✔ Writing '.codespellrc'.\n"
"✔ Adding Codespell config to '.codespellrc'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
)
Expand All @@ -74,8 +81,14 @@ def test_none_backend_pyproject_toml(self, tmp_path: Path):
# Assert
assert result.exit_code == 0, result.output
assert (tmp_path / "pyproject.toml").exists()
# Check if tomli is needed based on current interpreter
current_version = PythonVersion.from_interpreter()
needs_tomli = current_version.to_short_tuple() < (3, 11)
if needs_tomli:
expected_deps = "☐ Add the dev dependencies 'codespell', 'tomli'.\n"
else:
expected_deps = "☐ Add the dev dependency 'codespell'.\n"
assert result.output == (
"☐ Add the dev dependency 'codespell'.\n"
"✔ Adding Codespell config to 'pyproject.toml'.\n"
expected_deps + "✔ Adding Codespell config to 'pyproject.toml'.\n"
"☐ Run 'codespell' to run the Codespell spellchecker.\n"
)