Skip to content

Commit 77c90b2

Browse files
committed
feat: modernize tooling to uv, ruff, ty, and dependency-groups
- Switch from setuptools to hatchling build backend - Update requires-python from >=3.9 to >=3.11 - Replace mypy with ty for type checking - Migrate from optional-dependencies to dependency-groups (PEP 735) - Update CI to use uv, ruff, ty with separate lint/test/typecheck jobs - Fix mypy error in core.py by adding cast() - Apply ruff fixes: Path.open(), datetime.UTC, formatting - Add coverage threshold (70%) to pytest config - Update classifiers with valid Topic classifier
1 parent e7a3f82 commit 77c90b2

4 files changed

Lines changed: 540 additions & 65 deletions

File tree

.github/workflows/ci.yml

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,56 @@ on:
77
branches: [main]
88

99
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: astral-sh/setup-uv@v5
15+
with:
16+
python-version: "3.12"
17+
- name: Install dev dependencies
18+
run: uv sync --group lint
19+
- name: Ruff format check
20+
run: uv run ruff format --check .
21+
- name: Ruff lint
22+
run: uv run ruff check .
23+
1024
test:
1125
runs-on: ubuntu-latest
12-
timeout-minutes: 15
1326
strategy:
1427
matrix:
15-
python-version: ["3.10", "3.11", "3.12", "3.13"]
16-
28+
python-version: ["3.11", "3.12", "3.13"]
1729
steps:
18-
- uses: actions/checkout@v4
19-
20-
- name: Set up Python ${{ matrix.python-version }}
21-
uses: actions/setup-python@v5
22-
with:
23-
python-version: ${{ matrix.python-version }}
24-
cache: 'pip'
25-
26-
- name: Install dependencies
27-
run: |
28-
python -m pip install --upgrade pip
29-
if [ -f pyproject.toml ]; then
30-
pip install -e ".[dev]"
31-
fi
32-
if [ -f requirements-dev.txt ]; then
33-
pip install -r requirements-dev.txt
34-
fi
35-
36-
- name: Lint with ruff
37-
run: ruff check .
38-
39-
- name: Format check with ruff
40-
run: ruff format --check .
41-
42-
- name: Run tests
43-
run: pytest -v --tb=short -x
30+
- uses: actions/checkout@v4
31+
- uses: astral-sh/setup-uv@v5
32+
with:
33+
python-version: ${{ matrix.python-version }}
34+
- name: Install package with dev deps
35+
run: uv sync --all-groups
36+
- name: Run tests with coverage
37+
run: uv run pytest --tb=short -q
38+
39+
typecheck:
40+
runs-on: ubuntu-latest
41+
steps:
42+
- uses: actions/checkout@v4
43+
- uses: astral-sh/setup-uv@v5
44+
with:
45+
python-version: "3.12"
46+
- name: Install type checking dependencies
47+
run: uv sync --group typecheck
48+
- name: Run ty type checker
49+
run: uv run ty check dotfile_sync/
50+
51+
build:
52+
runs-on: ubuntu-latest
53+
needs: [test, lint, typecheck]
54+
steps:
55+
- uses: actions/checkout@v4
56+
- uses: astral-sh/setup-uv@v5
57+
with:
58+
python-version: "3.11"
59+
- name: Build package
60+
run: uv build
61+
- name: Verify package
62+
run: uv run dotfile-sync --version

dotfile_sync/core.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import json
66
import os
77
import shutil
8-
from datetime import datetime, timezone
8+
from datetime import UTC, datetime
99
from pathlib import Path
10-
from typing import Any
10+
from typing import Any, cast
1111

1212
from git import Repo
1313

@@ -35,34 +35,32 @@ def __init__(self, manifest_path: Path) -> None:
3535

3636
def _load(self) -> None:
3737
if self.manifest_path.exists():
38-
with open(self.manifest_path) as f:
38+
with Path(self.manifest_path).open() as f:
3939
self._data = json.load(f)
4040
else:
4141
self._data = {"version": "1.0", "files": []}
4242

4343
def save(self) -> None:
4444
self.manifest_path.parent.mkdir(parents=True, exist_ok=True)
45-
with open(self.manifest_path, "w") as f:
45+
with Path(self.manifest_path).open("w") as f:
4646
json.dump(self._data, f, indent=2)
4747
f.write("\n")
4848

4949
@property
5050
def files(self) -> list[dict[str, str]]:
51-
return self._data.get("files", [])
51+
return cast(list[dict[str, str]], self._data.get("files", []))
5252

5353
def add_file(self, original_path: str, repo_path: str) -> None:
5454
"""Add a file to the manifest."""
5555
# Check if already tracked
5656
for entry in self.files:
5757
if entry["original_path"] == original_path:
5858
return # Already tracked
59-
self._data["files"].append(
60-
{
61-
"original_path": original_path,
62-
"repo_path": repo_path,
63-
"added_at": datetime.now(timezone.utc).isoformat(),
64-
}
65-
)
59+
self._data["files"].append({
60+
"original_path": original_path,
61+
"repo_path": repo_path,
62+
"added_at": datetime.now(UTC).isoformat(),
63+
})
6664
self.save()
6765

6866
def remove_file(self, original_path: str) -> bool:
@@ -212,7 +210,7 @@ def backup(self, message: str | None = None) -> str:
212210
repo.index.add([str(self.files_dir)])
213211
repo.index.add([str(self.manifest_path)])
214212

215-
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
213+
timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")
216214
commit_msg = message or f"backup: {backed_up} file(s) at {timestamp}"
217215
repo.index.commit(commit_msg)
218216

pyproject.toml

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=68.0", "wheel"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
44

55
[project]
66
name = "dotfile-sync"
@@ -12,34 +12,35 @@ authors = [
1212
{ name = "jlaportebot" },
1313
]
1414
keywords = ["dotfiles", "backup", "sync", "cli", "git"]
15-
requires-python = ">=3.9"
15+
requires-python = ">=3.11"
1616
classifiers = [
1717
"Development Status :: 4 - Beta",
1818
"Environment :: Console",
1919
"Intended Audience :: Developers",
20+
"License :: OSI Approved :: MIT License",
2021
"Programming Language :: Python :: 3",
21-
"Programming Language :: Python :: 3.9",
22-
"Programming Language :: Python :: 3.10",
2322
"Programming Language :: Python :: 3.11",
2423
"Programming Language :: Python :: 3.12",
2524
"Programming Language :: Python :: 3.13",
26-
"Topic :: System :: Backup",
25+
"Topic :: System :: Archiving :: Backup",
2726
"Topic :: Utilities",
2827
"Typing :: Typed",
2928
]
3029
dependencies = [
31-
"click>=8.0",
32-
"rich>=13.0",
30+
"click>=8.1",
31+
"rich>=13.7",
3332
"gitpython>=3.1",
3433
]
3534

36-
[project.optional-dependencies]
35+
[dependency-groups]
3736
dev = [
38-
"pytest>=7.0",
39-
"pytest-cov>=4.0",
40-
"ruff>=0.1",
41-
"mypy>=1.0",
37+
{include-group = "lint"},
38+
{include-group = "test"},
39+
{include-group = "typecheck"},
4240
]
41+
lint = ["ruff>=0.6.0"]
42+
test = ["pytest>=8.0.0", "pytest-cov>=5.0.0"]
43+
typecheck = ["ty>=0.0.50"]
4344

4445
[project.scripts]
4546
dotfile-sync = "dotfile_sync.cli:main"
@@ -49,21 +50,64 @@ Homepage = "https://github.com/jlaportebot/dotfile-sync"
4950
Repository = "https://github.com/jlaportebot/dotfile-sync"
5051
Issues = "https://github.com/jlaportebot/dotfile-sync/issues"
5152

52-
[tool.setuptools.packages.find]
53-
include = ["dotfile_sync*"]
54-
5553
[tool.ruff]
56-
target-version = "py39"
5754
line-length = 100
55+
target-version = "py311"
56+
preview = true
5857

5958
[tool.ruff.lint]
60-
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
59+
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "C4", "PTH"]
60+
ignore = [
61+
"D", # docstring rules (too strict)
62+
"COM812", # trailing comma in format string
63+
"ISC001", # redundant isinstance check
64+
"PLR0913", # too many arguments (common in CLI)
65+
"TRY003", # avoid specifying long messages outside exception
66+
"PLR0917", # too many positional arguments (CLI entry point)
67+
"PLR0915", # too many statements (CLI entry point)
68+
"FBT001", # boolean positional argument (Click options)
69+
"DOC501", # missing exception in docstring (CLI entry point)
70+
"PLW0717", # try clause too many statements (CLI entry point)
71+
"PLC0415", # import not at top level (lazy imports in CLI)
72+
"B904", # raise without from (SystemExit in CLI)
73+
"RUF003", # ambiguous dash in comments (Unicode)
74+
"RUF002", # ambiguous dash in docstrings (Unicode)
75+
"PLR2004", # magic value used in comparison (threshold constants)
76+
"C901", # function too complex (large modules)
77+
"PLR0912", # too many branches (large modules)
78+
"PLR0914", # too many local variables (large modules)
79+
"SIM108", # ternary operator suggestion (personal preference)
80+
"DTZ001", # datetime without tzinfo (explicit UTC handling)
81+
"PLR6104", # use -= operator (style preference)
82+
"UP045", # Use X | None for type annotations (Python 3.10+)
83+
"UP035", # typing.Dict deprecated (Python 3.10+)
84+
"UP006", # Use dict instead of Dict (Python 3.10+)
85+
"UP007", # Use X | Y for type annotations (Python 3.10+)
86+
]
87+
per-file-ignores = { "tests/*" = ["S101", "ANN201", "ANN001", "PLR6301", "PLR2004", "PLC1901", "F401", "INP001", "S105", "PLC2701", "DTZ001"] }
88+
89+
[tool.ruff.format]
90+
quote-style = "double"
91+
indent-style = "space"
6192

6293
[tool.pytest.ini_options]
6394
testpaths = ["tests"]
64-
addopts = "-v --tb=short"
95+
addopts = [
96+
"--cov=dotfile_sync",
97+
"--cov-report=term-missing",
98+
"--cov-fail-under=70",
99+
"-v",
100+
]
101+
filterwarnings = [
102+
"ignore::DeprecationWarning",
103+
]
104+
105+
[tool.ty.terminal]
106+
error-on-warning = true
107+
108+
[tool.ty.environment]
109+
python-version = "3.11"
65110

66-
[tool.mypy]
67-
python_version = "3.9"
68-
warn_return_any = true
69-
warn_unused_configs = true
111+
[tool.ty.rules]
112+
possibly-unresolved-reference = "error"
113+
unused-ignore-comment = "warn"

0 commit comments

Comments
 (0)