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,7 +1,7 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: v0.23.0
_src_path: gh:tsvikas/python-template
cli_framework: typer
cli_framework: cyclopts
format_tool: black
get_package_version_from_vcs: true
github_user: tsvikas
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- Skip repos marked as 'meta' for better compatibility with pre-commit configs
- Support repo URLs from any Git provider (not just GitHub) by using URL parsing instead of regex
- 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

## v0.4.0

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ urls.issues = "https://github.com/tsvikas/sync-with-uv/issues"

requires-python = ">=3.10"
dependencies = [
"typer >=0.16",
"cyclopts >=4",
"tomli >=2",
"colorama >=0.4.6"
]
Expand Down
2 changes: 1 addition & 1 deletion src/sync_with_uv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

from .cli import app

app(prog_name="sync-with-uv")
app()
232 changes: 98 additions & 134 deletions src/sync_with_uv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from pathlib import Path
from typing import Annotated

import typer
import cyclopts.types
from colorama import Fore, Style
from cyclopts import App, Parameter

from . import __version__
from .repo_data import load_user_mappings
from .sync_with_uv import load_uv_lock, process_precommit_text

app = typer.Typer()
app = App(name="sync-with-uv")
app.register_install_completion_command()


def get_colored_diff(diff_lines: list[str]) -> list[str]:
Expand All @@ -39,101 +40,48 @@
return output_lines


def _version_callback(value: bool) -> None: # noqa: FBT001
"""Print version and exit if requested."""
if value:
print(f"sync-with-uv {__version__}")
raise typer.Exit(0)


@app.command()
def process_precommit( # noqa: C901, PLR0912, PLR0913
@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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider keeping the new helper functions and cyclopts annotations, as they improve clarity and maintain idiomatic usage.

No suggestions—splitting the big function into _print_* helpers actually simplifies the main flow, and the cyclopts annotations (while more verbose) are idiomatic for that library.

*,
precommit_filename: Annotated[
Path,
typer.Option(
"-p",
"--pre-commit-config",
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
help="Path to .pre-commit-config.yaml file to update",
),
cyclopts.types.ResolvedExistingFile, Parameter(["-p", "--pre-commit-config"])
] = Path(".pre-commit-config.yaml"),
uv_lock_filename: Annotated[
Path,
typer.Option(
"-u",
"--uv-lock",
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
help="Path to uv.lock file containing package versions",
),
cyclopts.types.ResolvedExistingFile, Parameter(["-u", "--uv-lock"])
] = Path("uv.lock"),
*,
check: Annotated[
bool,
typer.Option(
"--check",
help="Don't write the file back, just return the status. "
"Return code 0 means nothing would change. "
"Return code 1 means some package versions would be updated. "
"Return code 123 means there was an internal error.",
),
] = False,
diff: Annotated[
bool,
typer.Option(
"--diff",
help="Don't write the file back, "
"just output a diff to indicate what changes would be made.",
),
] = False,
color: Annotated[
bool,
typer.Option(
help="Enable colored diff output. Only applies when --diff is given."
),
] = False,
quiet: Annotated[
bool,
typer.Option(
"-q",
"--quiet",
help="Stop emitting all non-critical output. "
"Error messages will still be emitted.",
),
] = False,
verbose: Annotated[
bool,
typer.Option(
"-v",
"--verbose",
help="Show detailed information about all packages, "
"including those that were not changed.",
),
] = False,
version: Annotated[ # noqa: ARG001
bool | None,
typer.Option(
"--version",
"-V",
callback=_version_callback,
is_eager=True,
help="Show the version and exit.",
),
] = None,
) -> None:
check: Annotated[bool, Parameter(negative="")] = False,
diff: Annotated[bool, Parameter(negative="")] = False,
color: bool = False,
quiet: Annotated[bool, Parameter(alias="-q")] = False,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider supporting '--quiet' long flag for consistency.

Adding '--quiet' as an alias would align with the convention used for other flags and improve usability.

Suggested change
quiet: Annotated[bool, Parameter(alias="-q")] = False,
quiet: Annotated[bool, Parameter(alias=("-q", "--quiet"))] = False,

verbose: Annotated[bool, Parameter(alias="-v")] = False,
) -> int:
"""Sync pre-commit hook versions with uv.lock.
Updates the 'rev' fields in .pre-commit-config.yaml to match the package
versions found in uv.lock, ensuring consistent versions for development tools.
Parameters
----------
precommit_filename:
Path to .pre-commit-config.yaml file to update
uv_lock_filename
Path to uv.lock file containing package versions
check
Don't write the file back, just return the status.
Return code 0 means nothing would change.
Return code 1 means some package versions would be updated.
Return code 123 means there was an internal error.
diff
Don't write the file back,
just output a diff to indicate what changes would be made.
color
Enable colored diff output. Only applies when --diff is given.
quiet
Stop emitting all non-critical output.
Error messages will still be emitted.
verbose
Show detailed information about all packages,
including those that were not changed.
"""
try:
user_repo_mappings, user_version_mappings = load_user_mappings()
Expand All @@ -142,49 +90,65 @@
fixed_text, changes = process_precommit_text(
precommit_text, uv_data, user_repo_mappings, user_version_mappings
)
except Exception as e:
# report the results / change files
if verbose:
_print_packages(changes)
# output a diff to to stdout
if diff:
_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")
# 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 107 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)
raise typer.Exit(123) from e
# report the results / change files
if verbose:
for package, change in changes.items():
if isinstance(change, tuple):
print(f"{package}: {change[0]} -> {change[1]}", file=sys.stderr)
elif change:
print(f"{package}: unchanged", file=sys.stderr)
else:
print(f"{package}: not managed in uv", file=sys.stderr)
print(file=sys.stderr)
# output a diff to to stdout
if diff:
diff_lines = list(
difflib.unified_diff(
precommit_text.splitlines(keepends=True),
fixed_text.splitlines(keepends=True),
fromfile=str(precommit_filename),
tofile=str(precommit_filename),
)
)
if color:
diff_lines = get_colored_diff(diff_lines)
print("\n".join(diff_lines))
# update the file
if not diff and not check:
precommit_filename.write_text(fixed_text, encoding="utf-8")
# print summary
if verbose or not quiet:
print("All done!", file=sys.stderr)
n_changed = n_unchanged = 0
for change in changes.values():
if isinstance(change, tuple):
n_changed += 1
else:
n_unchanged += 1
would_be = "would be " if (diff or check) else ""
print(
f"{n_changed} package {would_be}changed, "
f"{n_unchanged} packages {would_be}left unchanged.",
file=sys.stderr,
return 123


def _print_packages(changes: dict[str, bool | tuple[str, str]]) -> None:
for package, change in changes.items():
if isinstance(change, tuple):
print(f"{package}: {change[0]} -> {change[1]}", file=sys.stderr)
elif change:
print(f"{package}: unchanged", file=sys.stderr)
else:
print(f"{package}: not managed in uv", file=sys.stderr)
print(file=sys.stderr)


def _print_diff(
precommit_text: str, fixed_text: str, precommit_filename: Path, *, color: bool
) -> None:
diff_lines = list(
difflib.unified_diff(
precommit_text.splitlines(keepends=True),
fixed_text.splitlines(keepends=True),
fromfile=str(precommit_filename),
tofile=str(precommit_filename),
)
# return 1 if check and changed
raise typer.Exit(check and fixed_text != precommit_text)
)
if color:
diff_lines = get_colored_diff(diff_lines)
print("\n".join(diff_lines))


def _print_summary(
changes: dict[str, bool | tuple[str, str]], *, dry_mode: bool
) -> None:
print("All done!", file=sys.stderr)
n_changed = n_unchanged = 0
for change in changes.values():
if isinstance(change, tuple):
n_changed += 1
else:
n_unchanged += 1
would_be = "would be " if dry_mode else ""
print(
f"{n_changed} package {would_be}changed, "
f"{n_unchanged} packages {would_be}left unchanged.",
file=sys.stderr,
)
Loading
Loading