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
11 changes: 11 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ $ pipx install --suffix=@next 'vcspull' --pip-args '\--pre' --force

<!-- Maintainers, insert changes / features for the next release here -->

### Development

#### chore: Implement PEP 563 deferred annotation resolution (#459)

- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking.
- Enable Ruff checks for PEP-compliant annotations:
- [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/)
- [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/)

For more details on PEP 563, see: https://peps.python.org/pep-0563/

## vcspull v1.33.0 (2024-11-23)

_Maintenance only, no bug fixes, or new features_
Expand Down
6 changes: 5 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
https://docs.pytest.org/en/stable/deprecations.html
"""

import pathlib
from __future__ import annotations

import shutil
import typing as t

import pytest

if t.TYPE_CHECKING:
import pathlib


@pytest.fixture(autouse=True)
def add_doctest_fixtures(
Expand Down
8 changes: 5 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Sphinx configuration for vcspull documentation."""

# flake8: noqa: E501
from __future__ import annotations

import inspect
import pathlib
import sys
Expand Down Expand Up @@ -146,7 +148,7 @@
}


def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
"""
Determine the URL corresponding to Python object.

Expand Down Expand Up @@ -216,13 +218,13 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
)


def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
def remove_tabs_js(app: Sphinx, exc: Exception) -> None:
"""Fix for sphinx-inline-tabs#18."""
if app.builder.format == "html" and not exc:
tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js"
tabs_js.unlink(missing_ok=True)


def setup(app: "Sphinx") -> None:
def setup(app: Sphinx) -> None:
"""Sphinx setup hook."""
app.connect("build-finished", remove_tabs_js)
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ exclude_lines = [
"if TYPE_CHECKING:",
"if t.TYPE_CHECKING:",
"@overload( |$)",
"from __future__ import annotations",
]

[tool.ruff]
Expand All @@ -201,10 +202,16 @@ select = [
"PERF", # Perflint
"RUF", # Ruff-specific rules
"D", # pydocstyle
"FA100", # future annotations
]
ignore = [
"COM812", # missing trailing comma, ruff format conflict
]
extend-safe-fixes = [
"UP006",
"UP007",
]
pyupgrade.keep-runtime-typing = false

[tool.ruff.lint.pydocstyle]
convention = "numpy"
Expand All @@ -214,6 +221,9 @@ known-first-party = [
"vcspull",
]
combine-as-imports = true
required-imports = [
"from __future__ import annotations",
]

[tool.ruff.lint.per-file-ignores]
"*/__init__.py" = ["F401"]
Expand Down
7 changes: 6 additions & 1 deletion scripts/generate_gitlab.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
#!/usr/bin/env python
"""Example script for export gitlab organization to vcspull config file."""

from __future__ import annotations

import argparse
import logging
import os
import pathlib
import sys
import typing as t

import requests
import yaml
from libvcs.sync.git import GitRemote

from vcspull.cli.sync import CouldNotGuessVCSFromURL, guess_vcs
from vcspull.types import RawConfig

if t.TYPE_CHECKING:
from vcspull.types import RawConfig

log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format="%(message)s")
Expand Down
2 changes: 2 additions & 0 deletions src/vcspull/__about__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Metadata for vcspull."""

from __future__ import annotations

__title__ = "vcspull"
__package_name__ = "vcspull"
__description__ = "Manage and sync multiple git, mercurial, and svn repos"
Expand Down
2 changes: 2 additions & 0 deletions src/vcspull/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"""

# Set default logging handler to avoid "No handler found" warnings.
from __future__ import annotations

import logging
from logging import NullHandler

Expand Down
16 changes: 9 additions & 7 deletions src/vcspull/_internal/config_reader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import json
import pathlib
import typing as t
Expand All @@ -22,11 +24,11 @@ class ConfigReader:
'{\n "session_name": "my session"\n}'
"""

def __init__(self, content: "RawConfigData") -> None:
def __init__(self, content: RawConfigData) -> None:
self.content = content

@staticmethod
def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]:
def _load(fmt: FormatLiteral, content: str) -> dict[str, t.Any]:
"""Load raw config data and directly return it.

>>> ConfigReader._load("json", '{ "session_name": "my session" }')
Expand All @@ -49,7 +51,7 @@ def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]:
raise NotImplementedError(msg)

@classmethod
def load(cls, fmt: "FormatLiteral", content: str) -> "ConfigReader":
def load(cls, fmt: FormatLiteral, content: str) -> ConfigReader:
"""Load raw config data into a ConfigReader instance (to dump later).

>>> cfg = ConfigReader.load("json", '{ "session_name": "my session" }')
Expand Down Expand Up @@ -118,7 +120,7 @@ def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
)

@classmethod
def from_file(cls, path: pathlib.Path) -> "ConfigReader":
def from_file(cls, path: pathlib.Path) -> ConfigReader:
r"""Load data from file path.

**YAML file**
Expand Down Expand Up @@ -159,8 +161,8 @@ def from_file(cls, path: pathlib.Path) -> "ConfigReader":

@staticmethod
def _dump(
fmt: "FormatLiteral",
content: "RawConfigData",
fmt: FormatLiteral,
content: RawConfigData,
indent: int = 2,
**kwargs: t.Any,
) -> str:
Expand All @@ -187,7 +189,7 @@ def _dump(
msg = f"{fmt} not supported in config"
raise NotImplementedError(msg)

def dump(self, fmt: "FormatLiteral", indent: int = 2, **kwargs: t.Any) -> str:
def dump(self, fmt: FormatLiteral, indent: int = 2, **kwargs: t.Any) -> str:
r"""Dump via ConfigReader instance.

>>> cfg = ConfigReader({ "session_name": "my session" })
Expand Down
6 changes: 4 additions & 2 deletions src/vcspull/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""CLI utilities for vcspull."""

from __future__ import annotations

import argparse
import logging
import textwrap
Expand Down Expand Up @@ -41,7 +43,7 @@ def create_parser(return_subparsers: t.Literal[False]) -> argparse.ArgumentParse

def create_parser(
return_subparsers: bool = False,
) -> t.Union[argparse.ArgumentParser, tuple[argparse.ArgumentParser, t.Any]]:
) -> argparse.ArgumentParser | tuple[argparse.ArgumentParser, t.Any]:
"""Create CLI argument parser for vcspull."""
parser = argparse.ArgumentParser(
prog="vcspull",
Expand Down Expand Up @@ -76,7 +78,7 @@ def create_parser(
return parser


def cli(_args: t.Optional[list[str]] = None) -> None:
def cli(_args: list[str] | None = None) -> None:
"""CLI entry point for vcspull."""
parser, sync_parser = create_parser(return_subparsers=True)
args = parser.parse_args(_args)
Expand Down
22 changes: 13 additions & 9 deletions src/vcspull/cli/sync.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
"""Synchronization functionality for vcspull."""

import argparse
from __future__ import annotations

import logging
import pathlib
import sys
import typing as t
from copy import deepcopy
from datetime import datetime

from libvcs._internal.shortcuts import create_project
from libvcs._internal.types import VCSLiteral
from libvcs.sync.git import GitSync
from libvcs.url import registry as url_tools

from vcspull import exc
from vcspull.config import filter_repos, find_config_files, load_configs

if t.TYPE_CHECKING:
import argparse
import pathlib
from datetime import datetime

from libvcs._internal.types import VCSLiteral
from libvcs.sync.git import GitSync

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -63,9 +68,8 @@ def sync(
repo_patterns: list[str],
config: pathlib.Path,
exit_on_error: bool,
parser: t.Optional[
argparse.ArgumentParser
] = None, # optional so sync can be unit tested
parser: argparse.ArgumentParser
| None = None, # optional so sync can be unit tested
) -> None:
"""Entry point for ``vcspull sync``."""
if isinstance(repo_patterns, list) and len(repo_patterns) == 0:
Expand Down Expand Up @@ -117,7 +121,7 @@ def progress_cb(output: str, timestamp: datetime) -> None:
sys.stdout.flush()


def guess_vcs(url: str) -> t.Optional[VCSLiteral]:
def guess_vcs(url: str) -> VCSLiteral | None:
"""Guess the VCS from a URL."""
vcs_matches = url_tools.registry.match(url=url, is_explicit=True)

Expand Down
Loading
Loading