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
13 changes: 13 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ Workflow actions moved to their current major releases: `actions/checkout` v7,
and `dorny/paths-filter` v4. Workflow behavior is unchanged, though setup-uv no
longer prunes the uv cache, so the first run after this repopulates it.

#### Ruff floor raised to 0.16 (#568)

Minimum `ruff>=0.16.0` (was unpinned). `ruff format` now formats Python code
blocks inside Markdown, so documentation examples are held to the same
formatting gate as source files.

Linting also adopts ruff's curated default rule set, which the project's
explicit `select` had been replacing rather than extending. Its findings are
fixed rather than silenced, with two exceptions kept as per-file ignores that
carry their reason in `pyproject.toml`: the Sphinx `conf.py` `exec` that reads
version metadata, and the deliberate catch-alls guarding the sync worker
thread, the log formatter, interpreter shutdown, and the dependency smoke test.

## vcspull v1.66.0 (2026-07-25)

vcspull v1.66.0 lets you register a repository before you have cloned it.
Expand Down
6 changes: 4 additions & 2 deletions docs/_ext/vcspull_output_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ class VcspullOutputLexer(RegexLexer):
(r"\bfailed\b", Generic.Error),
# Labels (muted) - common vcspull output labels
(
r"(Summary:|Progress:|Path:|Branch:|url:|workspace:|Ahead/Behind:|"
r"Remote:|Repository:|Note:|Usage:|Plan:|Tip:)",
(
r"(Summary:|Progress:|Path:|Branch:|url:|workspace:|Ahead/Behind:|"
r"Remote:|Repository:|Note:|Usage:|Plan:|Tip:)"
),
Generic.Heading,
),
# vcspull command and subcommands (for pretty docs)
Expand Down
4 changes: 2 additions & 2 deletions docs/internals/api/private_path.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ internal logic.
from vcspull._internal.private_path import PrivatePath

home_repo = PrivatePath("~/code/vcspull")
print(home_repo) # -> ~/code/vcspull
print(repr(home_repo)) # -> "PrivatePath('~/code/vcspull')"
print(home_repo) # -> ~/code/vcspull
print(repr(home_repo)) # -> "PrivatePath('~/code/vcspull')"
```

## Usage guidelines
Expand Down
31 changes: 27 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ dev = [
"coverage",
"pytest-cov",
# Lint
"ruff",
"ruff>=0.16.0",
"mypy",
# Annotations
"types-docutils",
Expand Down Expand Up @@ -120,7 +120,7 @@ coverage =[
"pytest-cov",
]
lint = [
"ruff",
"ruff>=0.16.0",
"mypy",
]
typings = [
Expand Down Expand Up @@ -209,7 +209,10 @@ exclude_lines = [
target-version = "py310"

[tool.ruff.lint]
select = [
# `select` is deliberately unset: ruff 0.16 enables a curated default rule
# set, and an explicit `select` would replace it rather than extend it.
# `extend-select` layers this project's additional linters on top.
extend-select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
Expand All @@ -226,7 +229,7 @@ select = [
"PERF", # Perflint
"RUF", # Ruff-specific rules
"D", # pydocstyle
"FA100", # future annotations
"FA100", # future annotations
]
ignore = [
"COM812", # missing trailing comma, ruff format conflict
Expand Down Expand Up @@ -257,6 +260,26 @@ required-imports = [

[tool.ruff.lint.per-file-ignores]
"*/__init__.py" = ["F401"]
# Sphinx conf.py reads the package's version metadata by exec'ing
# src/vcspull/__about__.py, so conf.py stays importable without the
# package installed. The input is a file in this repository.
"docs/conf.py" = ["S102"]
# The smoke test's job is to report any failure from importing an
# arbitrary module or invoking the CLI, so the catch has to be as wide
# as the failures it is probing for.
"scripts/runtime_dep_smoketest.py" = ["BLE001"]
# atexit handlers that raise are swallowed by the interpreter, so the
# cursor-restore handler logs and swallows to leave a breadcrumb
# without masking other shutdown tasks.
"src/vcspull/cli/_progress.py" = ["BLE001"]
# The sync worker thread catches everything, including BaseException,
# to hand the failure back to the main thread; narrowing would lose
# the exception instead of reporting it.
"src/vcspull/cli/sync.py" = ["BLE001"]
# A log formatter that raises takes the application down with it, so
# %-format failures in a caller's record become a "Bad message" line
# rather than an exception.
"src/vcspull/log.py" = ["BLE001"]

[tool.pytest.ini_options]
addopts = "--tb=short --no-header --showlocals --doctest-modules"
Expand Down
1 change: 0 additions & 1 deletion src/vcspull/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python
"""Manage multiple git, mercurial, svn repositories from a YAML / JSON file.

:copyright: Copyright 2013-2018 Tony Narlock.
Expand Down
4 changes: 3 additions & 1 deletion src/vcspull/_internal/private_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import typing as t

if t.TYPE_CHECKING:
from typing_extensions import Self

PrivatePathBase = pathlib.Path
else:
PrivatePathBase = type(pathlib.Path())
Expand Down Expand Up @@ -34,7 +36,7 @@ class PrivatePath(PrivatePathBase):
'~/notes.txt'
"""

def __new__(cls, *args: t.Any, **kwargs: t.Any) -> PrivatePath:
def __new__(cls, *args: t.Any, **kwargs: t.Any) -> Self:
return super().__new__(cls, *args, **kwargs)

@classmethod
Expand Down
9 changes: 7 additions & 2 deletions src/vcspull/cli/_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@

from ._colors import ColorMode, Colors, get_color_mode

if t.TYPE_CHECKING:
import types

from typing_extensions import Self

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -580,15 +585,15 @@ def __init__(self, indicator: SyncStatusIndicator, name: str) -> None:
self._indicator = indicator
self._name = name

def __enter__(self) -> _RepoContext:
def __enter__(self) -> Self:
self._indicator.start_repo(self._name)
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: t.Any,
tb: types.TracebackType | None,
) -> None:
self._indicator.stop_repo()

Expand Down
12 changes: 7 additions & 5 deletions src/vcspull/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -1983,10 +1983,12 @@ def _emit_summary(
unmatched = summary.get("unmatched", 0)
timed_out = summary.get("timed_out", 0)
parts = [
f"\n{colors.info('Summary:')} "
f"{colors.info(str(summary['total']))} repos, "
f"{colors.success(str(summary['synced']))} synced, "
f"{colors.error(str(summary['failed']))} failed",
(
f"\n{colors.info('Summary:')} "
f"{colors.info(str(summary['total']))} repos, "
f"{colors.success(str(summary['synced']))} synced, "
f"{colors.error(str(summary['failed']))} failed"
),
]
if timed_out > 0:
parts.append(
Expand Down Expand Up @@ -2041,7 +2043,7 @@ class CouldNotGuessVCSFromURL(exc.VCSPullException):
"""Raised when no VCS could be guessed from a URL."""

def __init__(self, repo_url: str, *args: object, **kwargs: object) -> None:
return super().__init__(f"Could not automatically determine VCS for {repo_url}")
super().__init__(f"Could not automatically determine VCS for {repo_url}")


class SyncFailedError(exc.VCSPullException):
Expand Down
2 changes: 1 addition & 1 deletion src/vcspull/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def default_debug_log_path() -> pathlib.Path:
>>> path.parent.parent == pathlib.Path(tempfile.gettempdir())
True
"""
stamp = datetime.now().strftime("%Y%m%dT%H%M%S")
stamp = datetime.now().astimezone().strftime("%Y%m%dT%H%M%S")
pid = os.getpid()
subdir = "vcspull-test" if "PYTEST_CURRENT_TEST" in os.environ else "vcspull"
return pathlib.Path(tempfile.gettempdir()) / subdir / f"debug-{stamp}-{pid}.log"
Expand Down
8 changes: 5 additions & 3 deletions tests/_internal/remotes/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

import pytest

if t.TYPE_CHECKING:
from typing_extensions import Self


class MockHTTPResponse:
"""Mock HTTP response for testing."""
Expand All @@ -32,13 +35,12 @@ def getheaders(self) -> list[tuple[str, str]]:
"""Return response headers as list of tuples."""
return list(self._headers.items())

def __enter__(self) -> MockHTTPResponse:
def __enter__(self) -> Self:
"""Context manager entry."""
return self

def __exit__(self, *args: t.Any) -> None:
def __exit__(self, *args: object) -> None:
"""Context manager exit."""
pass


@pytest.fixture
Expand Down
3 changes: 3 additions & 0 deletions tests/cli/test_sync_log_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ def now(cls) -> t.Any:
cls.value += 1

class _Stamp:
def astimezone(self) -> t.Any:
return self

@staticmethod
def strftime(_fmt: str) -> str:
return f"2026042400000{_Fake.value}"
Expand Down
4 changes: 0 additions & 4 deletions tests/cli/test_sync_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import io
import time
import typing as t

import pytest

Expand All @@ -13,9 +12,6 @@
build_indicator,
)

if t.TYPE_CHECKING:
pass


def test_disabled_indicator_is_silent_noop() -> None:
"""An indicator with ``enabled=False`` writes nothing to the stream."""
Expand Down
1 change: 1 addition & 0 deletions tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def test_git_commit_works_in_subprocess(
cwd=repo_dir,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, (
f"git commit failed: {result.stderr}\n"
Expand Down
1 change: 1 addition & 0 deletions tests/test_worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,7 @@ def test_ref_exists_remote_branch_fallback(
["git", "remote", "get-url", "origin"],
cwd=git_repo.path,
capture_output=True,
check=False,
)
if result.returncode == 0:
subprocess.run(
Expand Down
Loading