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 .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.14']
python-version: ['3.10', '3.14']
steps:
- uses: actions/checkout@v4
- name: Install uv
Expand Down
8 changes: 8 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ $ pip install --user --upgrade --pre libvcs

_Upcoming changes will be written here._

### Breaking changes

- Drop support for Python 3.9; the new minimum is Python 3.10 (#497).

See also:
- [Python 3.9 EOL timeline](https://devguide.python.org/versions/#:~:text=Release%20manager-,3.9,-PEP%20596)
- [PEP 596](https://peps.python.org/pep-0596/)

### Development

- Add Python 3.14 to test matrix (#496)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ detection and parsing of URLs, commanding, and syncing with `git`, `hg`, and `sv
Python API.
- **py.test fixtures**: Create temporary local repositories and working copies for testing for unit tests.

_Supports Python 3.9 and above, Git (including AWS CodeCommit), Subversion, and Mercurial._
_Supports Python 3.10 and above, Git (including AWS CodeCommit), Subversion, and Mercurial._

To **get started**, see the [quickstart guide](https://libvcs.git-pull.com/quickstart.html) for more information.

Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "libvcs"
version = "0.36.0"
description = "Lite, typed, python utilities for Git, SVN, Mercurial, etc."
requires-python = ">=3.9,<4.0"
requires-python = ">=3.10,<4.0"
authors = [
{name = "Tony Narlock", email = "tony@git-pull.com"}
]
Expand Down Expand Up @@ -36,7 +36,6 @@ classifiers = [
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -130,6 +129,7 @@ build-backend = "hatchling.build"

[tool.mypy]
strict = true
python_version = "3.10"
files = [
"src",
"tests",
Expand Down Expand Up @@ -158,7 +158,7 @@ exclude_lines = [
]

[tool.ruff]
target-version = "py39"
target-version = "py310"

[tool.ruff.lint]
select = [
Expand Down
8 changes: 4 additions & 4 deletions src/libvcs/_internal/query_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def keygetter(
dct = getattr(dct, sub_field)
except Exception as e:
traceback.print_stack()
logger.debug(f"The above error was {e}")
logger.debug("The above error was %s", e)
return None
return dct

Expand Down Expand Up @@ -122,12 +122,12 @@ def parse_lookup(obj: Mapping[str, t.Any], path: str, lookup: str) -> t.Any | No
"""
try:
if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
field_name = path.rsplit(lookup)[0]
field_name = path.split(lookup, maxsplit=1)[0]
if field_name is not None:
return keygetter(obj, field_name)
except Exception as e:
traceback.print_stack()
logger.debug(f"The above error was {e}")
logger.debug("The above error was %s", e)
return None


Expand Down Expand Up @@ -489,7 +489,7 @@ def __eq__(
return False

if len(self) == len(data):
for a, b in zip(self, data):
for a, b in zip(self, data, strict=False):
if isinstance(a, Mapping):
a_keys = a.keys()
if a.keys == b.keys():
Expand Down
11 changes: 4 additions & 7 deletions src/libvcs/_internal/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def console_to_str(s: bytes) -> str:

if t.TYPE_CHECKING:
_LoggerAdapter = logging.LoggerAdapter[logging.Logger]
from typing_extensions import TypeAlias
from typing import TypeAlias
else:
_LoggerAdapter = logging.LoggerAdapter

Expand Down Expand Up @@ -98,13 +98,10 @@ def __call__(self, output: str, timestamp: datetime.datetime) -> None:
if sys.platform == "win32":
_ENV: TypeAlias = Mapping[str, str]
else:
_ENV: TypeAlias = t.Union[
Mapping[bytes, StrPath],
Mapping[str, StrPath],
]
_ENV: TypeAlias = Mapping[bytes, StrPath] | Mapping[str, StrPath]

_CMD = t.Union[StrPath, Sequence[StrPath]]
_FILE: TypeAlias = t.Optional[t.Union[int, t.IO[t.Any]]]
_CMD = StrPath | Sequence[StrPath]
_FILE: TypeAlias = int | t.IO[t.Any] | None


def run(
Expand Down
2 changes: 1 addition & 1 deletion src/libvcs/_internal/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from libvcs.url import registry as url_tools

if t.TYPE_CHECKING:
from typing_extensions import TypeGuard
from typing import TypeGuard

from libvcs._internal.run import ProgressCallbackProtocol
from libvcs._internal.types import StrPath, VCSLiteral
Expand Down
16 changes: 6 additions & 10 deletions src/libvcs/_internal/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from .dataclasses import SkipDefaultFieldsReprMixin

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias


F = t.TypeVar("F", bound=t.Callable[..., t.Any])
Expand All @@ -66,14 +66,11 @@ def __init__(self, output: str, *args: object) -> None:
if sys.platform == "win32":
_ENV: TypeAlias = Mapping[str, str]
else:
_ENV: TypeAlias = t.Union[
Mapping[bytes, StrOrBytesPath],
Mapping[str, StrOrBytesPath],
]
_FILE: TypeAlias = t.Union[None, int, t.IO[t.Any]]
_TXT: TypeAlias = t.Union[bytes, str]
_ENV: TypeAlias = Mapping[bytes, StrOrBytesPath] | Mapping[str, StrOrBytesPath]
_FILE: TypeAlias = None | int | t.IO[t.Any]
_TXT: TypeAlias = bytes | str
#: Command
_CMD: TypeAlias = t.Union[StrOrBytesPath, Sequence[StrOrBytesPath]]
_CMD: TypeAlias = StrOrBytesPath | Sequence[StrOrBytesPath]


@dataclasses.dataclass(repr=False)
Expand Down Expand Up @@ -191,8 +188,7 @@ class SubprocessCommand(SkipDefaultFieldsReprMixin):
start_new_session: bool = False
pass_fds: t.Any = ()
umask: int = -1
if sys.version_info >= (3, 10):
pipesize: int = -1
pipesize: int = -1
user: str | None = None
group: str | None = None
extra_groups: list[str] | None = None
Expand Down
11 changes: 3 additions & 8 deletions src/libvcs/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,12 @@
from os import PathLike

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias

StrPath: TypeAlias = t.Union[str, PathLike[str]] # stable
StrPath: TypeAlias = str | PathLike[str] # stable
""":class:`os.PathLike` or :class:`str`"""

StrOrBytesPath: TypeAlias = t.Union[
str,
bytes,
PathLike[str],
PathLike[bytes], # stable
]
StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
""":class:`os.PathLike`, :class:`str` or :term:`bytes-like object`"""


Expand Down
2 changes: 1 addition & 1 deletion src/libvcs/cmd/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from libvcs._internal.run import ProgressCallbackProtocol, run
from libvcs._internal.types import StrOrBytesPath, StrPath

_CMD = t.Union[StrOrBytesPath, Sequence[StrOrBytesPath]]
_CMD = StrOrBytesPath | Sequence[StrOrBytesPath]


class Git:
Expand Down
2 changes: 1 addition & 1 deletion src/libvcs/cmd/hg.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from libvcs._internal.run import ProgressCallbackProtocol, run
from libvcs._internal.types import StrOrBytesPath, StrPath

_CMD = t.Union[StrOrBytesPath, Sequence[StrOrBytesPath]]
_CMD = StrOrBytesPath | Sequence[StrOrBytesPath]


class HgColorType(enum.Enum):
Expand Down
6 changes: 3 additions & 3 deletions src/libvcs/cmd/svn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
from libvcs._internal.run import ProgressCallbackProtocol, run
from libvcs._internal.types import StrOrBytesPath, StrPath

_CMD = t.Union[StrOrBytesPath, Sequence[StrOrBytesPath]]
_CMD = StrOrBytesPath | Sequence[StrOrBytesPath]

DepthLiteral = t.Union[t.Literal["infinity", "empty", "files", "immediates"], None]
RevisionLiteral = t.Union[t.Literal["HEAD", "BASE", "COMMITTED", "PREV"], None]
DepthLiteral = t.Literal["infinity", "empty", "files", "immediates"] | None
RevisionLiteral = t.Literal["HEAD", "BASE", "COMMITTED", "PREV"] | None


class SvnPropsetValueOrValuePathRequired(exc.LibVCSException, TypeError):
Expand Down
4 changes: 2 additions & 2 deletions src/libvcs/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from libvcs.sync.svn import SvnSync

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias

from libvcs._internal.run import _ENV

Expand Down Expand Up @@ -270,7 +270,7 @@ def unique_repo_name(remote_repos_path: pathlib.Path, max_retries: int = 15) ->
return remote_repo_name


InitCmdArgs: TypeAlias = t.Optional[list[str]]
InitCmdArgs: TypeAlias = list[str] | None


class CreateRepoPostInitFn(t.Protocol):
Expand Down
2 changes: 1 addition & 1 deletion src/libvcs/sync/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#: Default VCS systems by string (in :data:`DEFAULT_VCS_CLASS_MAP`)
DEFAULT_VCS_LITERAL = t.Literal["git", "hg", "svn"]
#: Union of VCS Classes
DEFAULT_VCS_CLASS_UNION = type[t.Union[GitSync, HgSync, SvnSync]]
DEFAULT_VCS_CLASS_UNION = type[GitSync | HgSync | SvnSync]
#: ``str`` -> ``class`` Map. ``DEFAULT_VCS_CLASS_MAP['git']`` ->
#: :class:`~libvcs.sync.git.GitSync`
DEFAULT_VCS_CLASS_MAP: dict[DEFAULT_VCS_LITERAL, DEFAULT_VCS_CLASS_UNION] = {
Expand Down
24 changes: 12 additions & 12 deletions src/libvcs/sync/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class GitRemote:


GitSyncRemoteDict = dict[str, GitRemote]
GitRemotesArgs = t.Union[None, GitSyncRemoteDict, dict[str, str]]
GitRemotesArgs = None | GitSyncRemoteDict | dict[str, str]


@dataclasses.dataclass
Expand Down Expand Up @@ -401,9 +401,9 @@ def update_repo(
self.log.debug("No git revision set, defaulting to origin/master")
symref = self.cmd.symbolic_ref(name="HEAD", short=True)
git_tag = symref.rstrip() if symref else "origin/master"
self.log.debug(f"git_tag: {git_tag}")
self.log.debug("git_tag: %s", git_tag)

self.log.info(f"Updating to '{git_tag}'.")
self.log.info("Updating to '%s'.", git_tag)

# Get head sha
try:
Expand All @@ -416,14 +416,14 @@ def update_repo(
self.log.exception("Failed to get the hash for HEAD")
return

self.log.debug(f"head_sha: {head_sha}")
self.log.debug("head_sha: %s", head_sha)

# If a remote ref is asked for, which can possibly move around,
# we must always do a fetch and checkout.
show_ref_output = self.cmd.show_ref(pattern=git_tag, check_returncode=False)
self.log.debug(f"show_ref_output: {show_ref_output}")
self.log.debug("show_ref_output: %s", show_ref_output)
is_remote_ref = "remotes" in show_ref_output
self.log.debug(f"is_remote_ref: {is_remote_ref}")
self.log.debug("is_remote_ref: %s", is_remote_ref)

# show-ref output is in the form "<sha> refs/remotes/<remote>/<tag>"
# we must strip the remote from the tag.
Expand All @@ -441,8 +441,8 @@ def update_repo(
raise GitRemoteRefNotFound(git_tag=git_tag, ref_output=show_ref_output)
git_remote_name = m.group("git_remote_name")
git_tag = m.group("git_tag")
self.log.debug(f"git_remote_name: {git_remote_name}")
self.log.debug(f"git_tag: {git_tag}")
self.log.debug("git_remote_name: %s", git_remote_name)
self.log.debug("git_tag: %s", git_tag)

# This will fail if the tag does not exist (it probably has not
# been fetched yet).
Expand All @@ -456,7 +456,7 @@ def update_repo(
except exc.CommandError as e:
error_code = e.returncode if e.returncode is not None else 0
tag_sha = ""
self.log.debug(f"tag_sha: {tag_sha}")
self.log.debug("tag_sha: %s", tag_sha)

# Is the hash checkout out what we want?
somethings_up = (error_code, is_remote_ref, tag_sha != head_sha)
Expand All @@ -467,7 +467,7 @@ def update_repo(
try:
process = self.cmd.fetch(log_in_real_time=True, check_returncode=True)
except exc.CommandError:
self.log.exception(f"Failed to fetch repository '{url}'")
self.log.exception("Failed to fetch repository '%s'", url)
return

if is_remote_ref:
Expand All @@ -493,7 +493,7 @@ def update_repo(
try:
process = self.cmd.checkout(branch=git_tag)
except exc.CommandError:
self.log.exception(f"Failed to checkout tag: '{git_tag}'")
self.log.exception("Failed to checkout tag: '%s'", git_tag)
return

# Rebase changes from the remote branch
Expand Down Expand Up @@ -537,7 +537,7 @@ def update_repo(
try:
process = self.cmd.checkout(branch=git_tag)
except exc.CommandError:
self.log.exception(f"Failed to checkout tag: '{git_tag}'")
self.log.exception("Failed to checkout tag: '%s'", git_tag)
return

self.cmd.submodule.update(recursive=True, init=True, log_in_real_time=True)
Expand Down
4 changes: 2 additions & 2 deletions src/libvcs/url/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from libvcs._internal.module_loading import import_string

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias

from .base import URLProtocol

ParserLazyMap: TypeAlias = dict[str, t.Union[type[URLProtocol], str]]
ParserLazyMap: TypeAlias = dict[str, type[URLProtocol] | str]
ParserMap: TypeAlias = dict[str, type[URLProtocol]]

DEFAULT_PARSERS: ParserLazyMap = {
Expand Down
8 changes: 2 additions & 6 deletions tests/url/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@

if t.TYPE_CHECKING:
from collections.abc import Callable

from typing_extensions import TypeAlias
from typing import TypeAlias

ParserMatchLazy: TypeAlias = Callable[[str], registry.ParserMatch]
DetectVCSFixtureExpectedMatch: TypeAlias = t.Union[
registry.ParserMatch,
ParserMatchLazy,
]
DetectVCSFixtureExpectedMatch: TypeAlias = registry.ParserMatch | ParserMatchLazy


class DetectVCSFixture(t.NamedTuple):
Expand Down
Loading