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 .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: v0.23.0
_commit: v0.23.1
_src_path: gh:tsvikas/python-template
cli_framework: cyclopts
format_tool: black
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
id-token: write
steps:
- name: Download package distributions
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: Packages
path: dist/
Expand All @@ -70,7 +70,7 @@ jobs:
id-token: write
steps:
- name: Download package distributions
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: Packages
path: dist/
Expand Down
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ repos:
- id: debug-statements
- id: name-tests-test
args: [--pytest-test-first]
exclude: ^tests/test_precommit/dummy_module.py$
- id: requirements-txt-fixer
# Symlinks
- id: check-symlinks
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Trigger the hook also on changes of the pyproject.toml file
- Migrate CLI framework from typer to cyclopts
- Fix file write permission errors to return exit code 123 instead of raising unhandled exception
- Preserve original line endings (CRLF/LF) when processing pre-commit config files

## v0.4.0

Expand Down
17 changes: 10 additions & 7 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ update-deps: && list-outdated-deps
&& exit 1; \
}

# Audit dependencies
audit-deps:
uv run --all-extras --all-groups --with pip-audit pip-audit --skip-editable \
--ignore-vuln GHSA-4xh5-x5gv-qwph
# pip-audit ignored vuln:
# GHSA-4xh5-x5gv-qwph:
# vuln is in pip, which is not a pinned requirwement
# vuln is fixed in recent python versions
# see https://github.com/pypa/pip/issues/13607


### code quality ###

Expand Down Expand Up @@ -70,13 +80,6 @@ lint:
uv run ruff check
uv run dmypy run
uv run --all-extras --all-groups --with deptry deptry src/
uv run --all-extras --all-groups --with pip-audit pip-audit --skip-editable \
--ignore-vuln GHSA-4xh5-x5gv-qwph
# pip-audit ignored vuln:
# GHSA-4xh5-x5gv-qwph:
# vuln is in pip, which is not a pinned requirwement
# vuln is fixed in recent python versions
# see https://github.com/pypa/pip/issues/13607
uv run pre-commit run --all-files

# Run Pylint (slow, not used in other tasks)
Expand Down
6 changes: 4 additions & 2 deletions src/sync_with_uv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@


@app.default()
def process_precommit( # noqa: PLR0913

Check notice on line 44 in src/sync_with_uv/cli.py

View workflow job for this annotation

GitHub Actions / pylint

R0913

Too many arguments (7/5)
*,
precommit_filename: Annotated[
cyclopts.types.ResolvedExistingFile, Parameter(["-p", "--pre-commit-config"])
Expand Down Expand Up @@ -86,7 +86,9 @@
try:
user_repo_mappings, user_version_mappings = load_user_mappings()
uv_data = load_uv_lock(uv_lock_filename)
precommit_text = precommit_filename.read_text(encoding="utf-8")
# note that the next line can be simplified in Python>=3.13 using
# read_text with newline=""
precommit_text = precommit_filename.read_bytes().decode(encoding="utf-8")
fixed_text, changes = process_precommit_text(
precommit_text, uv_data, user_repo_mappings, user_version_mappings
)
Expand All @@ -98,13 +100,13 @@
_print_diff(precommit_text, fixed_text, precommit_filename, color=color)
# update the file
if not diff and not check:
precommit_filename.write_text(fixed_text, encoding="utf-8")
precommit_filename.write_text(fixed_text, encoding="utf-8", newline="")
# print summary
if verbose or not quiet:
_print_summary(changes, dry_mode=diff or check)
# return 1 if check and changed
return int(check and fixed_text != precommit_text)
except Exception as e: # noqa: BLE001

Check warning on line 109 in src/sync_with_uv/cli.py

View workflow job for this annotation

GitHub Actions / pylint

W0718

Catching too general exception Exception
print("Error:", e, file=sys.stderr)
return 123

Expand Down
14 changes: 6 additions & 8 deletions src/sync_with_uv/sync_with_uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)


def process_precommit_text(

Check notice on line 33 in src/sync_with_uv/sync_with_uv.py

View workflow job for this annotation

GitHub Actions / pylint

R0914

Too many local variables (18/15)
precommit_text: str,
uv_data: dict[str, str],
user_repo_mappings: dict[str, str] | None = None,
Expand All @@ -54,25 +54,23 @@
- repo URLs to False when no package mapping exists
"""
# NOTE: this only works if the 'repo' is the first key of the element
repo_header_re = re.compile(r"\s*-\s*repo\s*:\s*(\S*).*")
repo_rev_re = re.compile(r"\s*rev\s*:\s*(\S*).*")
lines = precommit_text.split("\n")
repo_header_re = re.compile(r"^\s*-\s*repo\s*:\s*(\S*).*$")
repo_rev_re = re.compile(r"^\s*rev\s*:\s*(\S*).*$")
lines = precommit_text.splitlines(keepends=True)
new_lines: list[str] = []
repo_url: str | None = None
package: str | None = None
changes: dict[str, bool | tuple[str, str]] = {}
for line in lines:
if repo_header := repo_header_re.fullmatch(line):
if repo_header := repo_header_re.match(line):
repo_url = repo_header.group(1)
package = repo_to_package(repo_url, user_repo_mappings)
if not package:
if repo_url not in {"local", "meta"}:
changes[repo_url] = False
elif package not in uv_data:
changes[package] = False
elif (
package and package in uv_data and (repo_rev := repo_rev_re.fullmatch(line))
):
elif package and package in uv_data and (repo_rev := repo_rev_re.match(line)):
assert repo_url is not None # noqa: S101
current_version = repo_rev.group(1)
version_template = repo_to_version_template(repo_url, user_version_mappings)
Expand All @@ -90,4 +88,4 @@
continue # don't add the line twice
new_lines.append(line)

return "\n".join(new_lines), changes
return "".join(new_lines), changes
47 changes: 47 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,50 @@ def test_cli_missing_precommit_config(
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "does not exist" in captured.err


@pytest.mark.parametrize(
"line_ending",
["\n", "\r\n", "\r"],
ids=["LF", "CRLF", "CR"],
)
def test_cli_preserves_line_endings_when_writing(
tmp_path: Path, line_ending: str, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test CLI preserves line endings when writing files (issue #24)."""
# Create uv.lock with package version
uv_lock_file = tmp_path / "uv.lock"
uv_lock_file.write_text(
"""
[[package]]
name = "black"
version = "24.0.0"
"""
)

# Create pre-commit config with specific line endings
precommit_file = tmp_path / ".pre-commit-config.yaml"
precommit_content = line_ending.join(
[
"repos:",
"- repo: https://github.com/psf/black-pre-commit-mirror",
" rev: 23.11.0",
" hooks:",
" - id: black",
]
)
# Write with binary mode to ensure exact line endings
precommit_content_bytes = precommit_content.encode("utf-8")
precommit_file.write_bytes(precommit_content_bytes)
assert precommit_content_bytes == precommit_file.read_bytes()

# Run CLI (version is already correct, so no changes needed)
with pytest.raises(SystemExit) as exc_info:
app(["-p", str(precommit_file), "-u", str(uv_lock_file)])
assert exc_info.value.code == 0
assert "All done" in capsys.readouterr().err

assert (
precommit_content_bytes.replace(b"23.11.0", b"24.0.0")
== precommit_file.read_bytes()
)
104 changes: 91 additions & 13 deletions tests/test_precommit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import textwrap
from pathlib import Path

import pytest

for GIT_BIN in [
Path("/usr/bin/git"),
Path(r"C:\Program Files\Git\bin\git.exe"),
Expand All @@ -13,36 +15,60 @@
raise RuntimeError("Could not find git binary") # noqa: TRY003


def test_precommit_hook(datadir: Path) -> None:
repo_dir = datadir

# initialize git repo
@pytest.fixture
def repo_with_precommit(tmp_path: Path) -> Path:
ruff = ("v0.0.200", "0.1.0")
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
subprocess.run([GIT_BIN, "init"], cwd=repo_dir, check=True)
subprocess.run(
[GIT_BIN, "config", "user.name", "Test User"], cwd=repo_dir, check=True
)
subprocess.run(
[GIT_BIN, "config", "user.email", "[email protected]"], cwd=repo_dir, check=True
)
repo_dir.joinpath(".pre-commit-config.yaml").write_text("repos:\n")
repo_dir.joinpath("uv.lock").write_text("version = 1\nrequires = []\n")
subprocess.run(["pre-commit", "install"], cwd=repo_dir, check=True) # noqa: S607

if ruff:
repo_dir.joinpath("dummy_module.py").write_text('print("Hello, world!")\n')
with repo_dir.joinpath("pyproject.toml").open("a") as f:
f.write('[tool.ruff]\ntarget-version = "py311"\n')
with repo_dir.joinpath("uv.lock").open("a") as f:
f.write(f'[[package]]\nname = "ruff"\nversion = "{ruff[1]}"\n')
with repo_dir.joinpath(".pre-commit-config.yaml").open("a") as f:
f.write(
" - repo: https://github.com/astral-sh/ruff-pre-commit\n"
f" rev: {ruff[0]}\n"
" hooks:\n"
" - id: ruff\n"
)
# stage and commit without sync-with-uv
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True)
subprocess.run([GIT_BIN, "commit", "-m", "old hooks"], cwd=repo_dir, check=True)

# add sync-with-uv
hook_config = textwrap.dedent(
return repo_dir


THIS_REPO_HOOKS = (
textwrap.dedent(
"""\
- repo: local
hooks:
"""
)
hook_config += textwrap.indent(
+ textwrap.indent(
Path(__file__).parents[1].joinpath(".pre-commit-hooks.yaml").read_text(), " "
)
hook_config = textwrap.indent(hook_config, " ")
)


def test_precommit_hook(repo_with_precommit: Path) -> None:
repo_dir = repo_with_precommit

# add sync-with-uv
with repo_dir.joinpath(".pre-commit-config.yaml").open("a") as f:
f.write(hook_config)
f.write(textwrap.indent(THIS_REPO_HOOKS, " "))

# commit and fail
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True, capture_output=True)
Expand All @@ -58,17 +84,69 @@ def test_precommit_hook(datadir: Path) -> None:
# check the updated .pre-commit-config.yaml
updated_config_path = repo_dir / ".pre-commit-config.yaml"
updated_config = updated_config_path.read_text()
expected_config = textwrap.dedent(
"""\
repos:
expected_config = textwrap.indent(
textwrap.dedent(
"""\
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.0
hooks:
- id: ruff
"""
),
" ",
)
assert expected_config in updated_config

# commit and succeed
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True)
subprocess.run([GIT_BIN, "commit", "-m", "new hooks"], cwd=repo_dir, check=True)


@pytest.mark.parametrize("line_ending", ["crlf", "lf"])
def test_precommit_hook_with_line_ending_fix(
repo_with_precommit: Path, line_ending: str
) -> None:
repo_dir = repo_with_precommit

mixed_lines_ending_hook = textwrap.dedent(
f"""\
- repo: https://github.com/pre-commit/pre-commit-hooks\r
rev: v6.0.0
hooks:
- id: mixed-line-ending
args: [--fix={line_ending}]
"""
)
# add mixed-line-ending
with repo_dir.joinpath(".pre-commit-config.yaml").open("a", newline="") as f:
f.write(textwrap.indent(mixed_lines_ending_hook, " "))
# commit and fail
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True, capture_output=True)
commit_process = subprocess.run( # noqa: PLW1510
[GIT_BIN, "commit", "-m", "failing commit - mixed line endings"],
cwd=repo_dir,
capture_output=True,
text=True,
)
assert commit_process.returncode == 1
assert ".....Failed" in commit_process.stderr
# commit and succeed
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True)
subprocess.run([GIT_BIN, "commit", "-m", "new hooks"], cwd=repo_dir, check=True)

# add sync-with-uv
with repo_dir.joinpath(".pre-commit-config.yaml").open("a") as f:
f.write(textwrap.indent(THIS_REPO_HOOKS, " "))
# commit and fail
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True, capture_output=True)
commit_process = subprocess.run( # noqa: PLW1510
[GIT_BIN, "commit", "-m", "failing commit - ruff version"],
cwd=repo_dir,
capture_output=True,
text=True,
)
assert commit_process.returncode == 1
assert ".....Failed" in commit_process.stderr
# commit and succeed
subprocess.run([GIT_BIN, "add", "."], cwd=repo_dir, check=True)
subprocess.run([GIT_BIN, "commit", "-m", "new hooks"], cwd=repo_dir, check=True)
5 changes: 0 additions & 5 deletions tests/test_precommit/.pre-commit-config.yaml

This file was deleted.

1 change: 0 additions & 1 deletion tests/test_precommit/dummy_module.py

This file was deleted.

2 changes: 0 additions & 2 deletions tests/test_precommit/pyproject.toml

This file was deleted.

5 changes: 0 additions & 5 deletions tests/test_precommit/uv.lock

This file was deleted.

Loading
Loading