From 6569d6614e897f3b9647dac4c7a9f7ae2b31c6e6 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Wed, 23 Jul 2025 20:20:12 +0200 Subject: [PATCH 1/9] fix typing in test folder also include test folder in mypy check in ci --- build.sh | 2 +- pygit2/_pygit2.pyi | 8 ++++++-- test/test_refs.py | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index 4327c1ae..31fea244 100644 --- a/build.sh +++ b/build.sh @@ -269,7 +269,7 @@ if [ "$1" = "mypy" ]; then $PREFIX/bin/pip install $WHEELDIR/pygit2*-$PYTHON_TAG-*.whl fi $PREFIX/bin/pip install -r requirements-test.txt - $PREFIX/bin/mypy pygit2 + $PREFIX/bin/mypy pygit2 test fi # Test .pyi stub file diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index fc8470c4..20c704d5 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -286,6 +286,8 @@ class Object: short_id: str type: 'Literal[GIT_OBJ_COMMIT] | Literal[GIT_OBJ_TREE] | Literal[GIT_OBJ_TAG] | Literal[GIT_OBJ_BLOB]' type_str: "Literal['commit'] | Literal['tree'] | Literal['tag'] | Literal['blob']" + author: Signature + tree: Tree @overload def peel( self, target_type: 'Literal[GIT_OBJ_COMMIT] | Type[Commit]' @@ -675,9 +677,11 @@ class Branches: def __getitem__(self, name: str) -> Branch: ... def get(self, key: str) -> Branch: ... def __iter__(self) -> Iterator[str]: ... - def create(self, name: str, commit: Commit, force: bool = False) -> Branch: ... + def create( + self, name: str, commit: Object | Commit, force: bool = False + ) -> Branch: ... def delete(self, name: str) -> None: ... - def with_commit(self, commit: Commit | _OidArg | None) -> 'Branches': ... + def with_commit(self, commit: Object | Commit | _OidArg | None) -> 'Branches': ... def __contains__(self, name: _OidArg) -> bool: ... class Repository: diff --git a/test/test_refs.py b/test/test_refs.py index 50dfa96e..a2fe0ef5 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -61,6 +61,7 @@ def test_refs_list(testrepo: Repository) -> None: def test_head(testrepo: Repository) -> None: head = testrepo.head assert LAST_COMMIT == testrepo[head.target].id + assert not isinstance(head.raw_target, bytes) assert LAST_COMMIT == testrepo[head.raw_target].id @@ -248,6 +249,7 @@ def test_refs_create_symbolic(testrepo: Repository) -> None: def test_refs_peel(testrepo: Repository) -> None: ref = testrepo.references.get('refs/heads/master') assert testrepo[ref.target].id == ref.peel().id + assert not isinstance(ref.raw_target, bytes) assert testrepo[ref.raw_target].id == ref.peel().id commit = ref.peel(Commit) From a59944b6445d127fc7d9d3f0a852de80cf8ced74 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Wed, 23 Jul 2025 20:47:33 +0200 Subject: [PATCH 2/9] improve types according to test/test_submodule.py --- pygit2/_pygit2.pyi | 16 ++++++++++++- pygit2/submodules.py | 10 ++++---- test/test_submodule.py | 52 ++++++++++++++++++++++-------------------- test/utils.py | 13 ++++++++--- 4 files changed, 58 insertions(+), 33 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 20c704d5..9d022967 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -7,6 +7,7 @@ from .enums import ( ApplyLocation, BranchType, BlobFilter, + CheckoutStrategy, DeltaStatus, DiffFind, DiffFlag, @@ -25,7 +26,9 @@ from .enums import ( from collections.abc import Generator from .repository import BaseRepository +from .submodules import SubmoduleCollection, Submodule from .remotes import Remote +from .callbacks import CheckoutCallbacks GIT_OBJ_BLOB = Literal[3] GIT_OBJ_COMMIT = Literal[1] @@ -700,10 +703,12 @@ class Repository: references: References remotes: RemoteCollection branches: Branches + submodules: SubmoduleCollection + index: Index def __init__(self, *args, **kwargs) -> None: ... def TreeBuilder(self, src: Tree | _OidArg = ...) -> TreeBuilder: ... def _disown(self, *args, **kwargs) -> None: ... - def _from_c(self, *args, **kwargs) -> None: ... + def _from_c(self, *args, **kwargs) -> 'Repository': ... def __getitem__(self, key: str | Oid) -> Object: ... def add_worktree(self, name: str, path: str, ref: Reference = ...) -> Worktree: ... def applies( @@ -715,6 +720,15 @@ class Repository: def apply( self, diff: Diff, location: ApplyLocation = ApplyLocation.WORKDIR ) -> None: ... + def checkout( + self, + refname: Optional[_OidArg], + *, + strategy: CheckoutStrategy | None = None, + directory: str | None = None, + paths: list[str] | None = None, + callbacks: CheckoutCallbacks | None = None, + ) -> None: ... def cherrypick(self, id: _OidArg) -> None: ... def compress_references(self) -> None: ... def create_blob(self, data: bytes) -> Oid: ... diff --git a/pygit2/submodules.py b/pygit2/submodules.py index ce83777b..2aec25ec 100644 --- a/pygit2/submodules.py +++ b/pygit2/submodules.py @@ -25,6 +25,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Iterable, Iterator, Optional, Union +from pathlib import Path from ._pygit2 import Oid from .callbacks import git_fetch_options, RemoteCallbacks @@ -36,6 +37,7 @@ # Need BaseRepository for type hints, but don't let it cause a circular dependency if TYPE_CHECKING: from .repository import BaseRepository + from pygit2 import Repository class Submodule: @@ -51,10 +53,10 @@ def _from_c(cls, repo: BaseRepository, cptr): return subm - def __del__(self): + def __del__(self) -> None: C.git_submodule_free(self._subm) - def open(self): + def open(self) -> Repository: """Open the repository for a submodule.""" crepo = ffi.new('git_repository **') err = C.git_submodule_open(crepo, self._subm) @@ -62,7 +64,7 @@ def open(self): return self._repo._from_c(crepo[0], True) - def init(self, overwrite: bool = False): + def init(self, overwrite: bool = False) -> None: """ Just like "git submodule init", this copies information about the submodule into ".git/config". @@ -173,7 +175,7 @@ class SubmoduleCollection: def __init__(self, repository: BaseRepository): self._repository = repository - def __getitem__(self, name: str) -> Submodule: + def __getitem__(self, name: str | Path) -> Submodule: """ Look up submodule information by name or path. Raises KeyError if there is no such submodule. diff --git a/test/test_submodule.py b/test/test_submodule.py index 235fed66..dcfc5829 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -26,8 +26,10 @@ """Tests for Submodule objects.""" from pathlib import Path +from typing import Generator import pygit2 +from pygit2 import Repository, Submodule import pytest from . import utils @@ -42,48 +44,48 @@ @pytest.fixture -def repo(tmp_path): +def repo(tmp_path: Path) -> Generator[Repository, None, None]: with utils.TemporaryRepository('submodulerepo.zip', tmp_path) as path: yield pygit2.Repository(path) -def test_lookup_submodule(repo): - s = repo.submodules[SUBM_PATH] +def test_lookup_submodule(repo: Repository) -> None: + s: Submodule | None = repo.submodules[SUBM_PATH] assert s is not None s = repo.submodules.get(SUBM_PATH) assert s is not None -def test_lookup_submodule_aspath(repo): +def test_lookup_submodule_aspath(repo: Repository) -> None: s = repo.submodules[Path(SUBM_PATH)] assert s is not None -def test_lookup_missing_submodule(repo): +def test_lookup_missing_submodule(repo: Repository) -> None: with pytest.raises(KeyError): repo.submodules['does-not-exist'] assert repo.submodules.get('does-not-exist') is None -def test_listall_submodules(repo): +def test_listall_submodules(repo: Repository) -> None: submodules = repo.listall_submodules() assert len(submodules) == 1 assert submodules[0] == SUBM_PATH -def test_contains_submodule(repo): +def test_contains_submodule(repo: Repository) -> None: assert SUBM_PATH in repo.submodules assert 'does-not-exist' not in repo.submodules -def test_submodule_iterator(repo): +def test_submodule_iterator(repo: Repository) -> None: for s in repo.submodules: assert isinstance(s, pygit2.Submodule) assert s.path == repo.submodules[s.path].path @utils.requires_network -def test_submodule_open(repo): +def test_submodule_open(repo: Repository) -> None: s = repo.submodules[SUBM_PATH] repo.submodules.init() repo.submodules.update() @@ -93,7 +95,7 @@ def test_submodule_open(repo): @utils.requires_network -def test_submodule_open_from_repository_subclass(repo): +def test_submodule_open_from_repository_subclass(repo: Repository) -> None: class CustomRepoClass(pygit2.Repository): pass @@ -106,22 +108,22 @@ class CustomRepoClass(pygit2.Repository): assert r.head.target == SUBM_HEAD_SHA -def test_name(repo): +def test_name(repo: Repository) -> None: s = repo.submodules[SUBM_PATH] assert SUBM_NAME == s.name -def test_path(repo): +def test_path(repo: Repository) -> None: s = repo.submodules[SUBM_PATH] assert SUBM_PATH == s.path -def test_url(repo): +def test_url(repo: Repository) -> None: s = repo.submodules[SUBM_PATH] assert SUBM_URL == s.url -def test_missing_url(repo): +def test_missing_url(repo: Repository) -> None: # Remove "url" from .gitmodules with open(Path(repo.workdir, '.gitmodules'), 'wt') as f: f.write('[submodule "TestGitRepository"]\n') @@ -131,7 +133,7 @@ def test_missing_url(repo): @utils.requires_network -def test_init_and_update(repo): +def test_init_and_update(repo: Repository) -> None: subrepo_file_path = Path(repo.workdir) / SUBM_PATH / 'master.txt' assert not subrepo_file_path.exists() @@ -148,7 +150,7 @@ def test_init_and_update(repo): @utils.requires_network -def test_specified_update(repo): +def test_specified_update(repo: Repository) -> None: subrepo_file_path = Path(repo.workdir) / SUBM_PATH / 'master.txt' assert not subrepo_file_path.exists() repo.submodules.init(submodules=['TestGitRepository']) @@ -157,7 +159,7 @@ def test_specified_update(repo): @utils.requires_network -def test_update_instance(repo): +def test_update_instance(repo: Repository) -> None: subrepo_file_path = Path(repo.workdir) / SUBM_PATH / 'master.txt' assert not subrepo_file_path.exists() sm = repo.submodules['TestGitRepository'] @@ -168,7 +170,7 @@ def test_update_instance(repo): @utils.requires_network @pytest.mark.parametrize('depth', [0, 1]) -def test_oneshot_update(repo, depth): +def test_oneshot_update(repo: Repository, depth: int) -> None: status = repo.submodules.status(SUBM_NAME) assert status == (SS.IN_HEAD | SS.IN_INDEX | SS.IN_CONFIG | SS.WD_UNINITIALIZED) @@ -190,7 +192,7 @@ def test_oneshot_update(repo, depth): @utils.requires_network @pytest.mark.parametrize('depth', [0, 1]) -def test_oneshot_update_instance(repo, depth): +def test_oneshot_update_instance(repo: Repository, depth: int) -> None: subrepo_file_path = Path(repo.workdir) / SUBM_PATH / 'master.txt' assert not subrepo_file_path.exists() sm = repo.submodules[SUBM_NAME] @@ -206,12 +208,12 @@ def test_oneshot_update_instance(repo, depth): @utils.requires_network -def test_head_id(repo): +def test_head_id(repo: Repository) -> None: assert repo.submodules[SUBM_PATH].head_id == SUBM_HEAD_SHA @utils.requires_network -def test_head_id_null(repo): +def test_head_id_null(repo: Repository) -> None: gitmodules_newlines = ( '\n' '[submodule "uncommitted_submodule"]\n' @@ -230,7 +232,7 @@ def test_head_id_null(repo): @utils.requires_network @pytest.mark.parametrize('depth', [0, 1]) -def test_add_submodule(repo, depth): +def test_add_submodule(repo: Repository, depth: int) -> None: sm_repo_path = 'test/testrepo' sm = repo.submodules.add(SUBM_URL, sm_repo_path, depth=depth) @@ -250,7 +252,7 @@ def test_add_submodule(repo, depth): @utils.requires_network -def test_submodule_status(repo): +def test_submodule_status(repo: Repository) -> None: common_status = SS.IN_HEAD | SS.IN_INDEX | SS.IN_CONFIG # Submodule needs initializing @@ -302,7 +304,7 @@ def test_submodule_status(repo): ) -def test_submodule_cache(repo): +def test_submodule_cache(repo: Repository) -> None: # When the cache is turned on, looking up the same submodule twice must return the same git_submodule object repo.submodules.cache_all() sm1 = repo.submodules[SUBM_NAME] @@ -317,7 +319,7 @@ def test_submodule_cache(repo): assert sm3._subm != sm4._subm -def test_submodule_reload(repo): +def test_submodule_reload(repo: Repository) -> None: sm = repo.submodules[SUBM_NAME] assert sm.url == 'https://github.com/libgit2/TestGitRepository' diff --git a/test/utils.py b/test/utils.py index 7c840f13..645a95bb 100644 --- a/test/utils.py +++ b/test/utils.py @@ -31,6 +31,8 @@ import stat import sys import zipfile +from typing import Optional +from types import TracebackType # Requirements import pytest @@ -94,11 +96,11 @@ def rmtree(path): class TemporaryRepository: - def __init__(self, name, tmp_path): + def __init__(self, name: str, tmp_path: Path) -> None: self.name = name self.tmp_path = tmp_path - def __enter__(self): + def __enter__(self) -> Path: path = Path(__file__).parent / 'data' / self.name temp_repo_path = Path(self.tmp_path) / path.stem if path.suffix == '.zip': @@ -111,7 +113,12 @@ def __enter__(self): return temp_repo_path - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: pass From 7528441a9d38096f776b33ff210942c61bcf422d Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Wed, 23 Jul 2025 22:03:01 +0200 Subject: [PATCH 3/9] improve types according to tests/test_apply_diff.py --- pygit2/_pygit2.pyi | 9 ++++++++ test/test_apply_diff.py | 48 +++++++++++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 9d022967..38c7545b 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -779,6 +779,15 @@ class Repository: def create_tag( self, name: str, oid: _OidArg, type: ObjectType, tagger: Signature, message: str ) -> Oid: ... + def diff( + self, + a: None | str | Reference = None, + b: None | str | Reference = None, + cached: bool = False, + flags: DiffOption = DiffOption.NORMAL, + context_lines: int = 3, + interhunk_lines: int = 0, + ) -> Diff: ... def descendant_of(self, oid1: _OidArg, oid2: _OidArg) -> bool: ... def expand_id(self, hex: str) -> Oid: ... def free(self) -> None: ... diff --git a/test/test_apply_diff.py b/test/test_apply_diff.py index 87e766dd..e5039324 100644 --- a/test/test_apply_diff.py +++ b/test/test_apply_diff.py @@ -24,6 +24,7 @@ # Boston, MA 02110-1301, USA. import pygit2 +from pygit2 import Repository, Diff from pygit2.enums import ApplyLocation, CheckoutStrategy, FileStatus import pytest @@ -31,31 +32,32 @@ from pathlib import Path -def read_content(testrepo): +def read_content(testrepo: Repository) -> str: with (Path(testrepo.workdir) / 'hello.txt').open('rb') as f: return f.read().decode('utf-8') @pytest.fixture -def new_content(): - content = ['bye world', 'adiós', 'au revoir monde'] - content = ''.join(x + os.linesep for x in content) +def new_content() -> str: + content_list = ['bye world', 'adiós', 'au revoir monde'] + content = ''.join(x + os.linesep for x in content_list) return content @pytest.fixture -def old_content(testrepo): +def old_content(testrepo: Repository) -> str: with (Path(testrepo.workdir) / 'hello.txt').open('rb') as f: return f.read().decode('utf-8') @pytest.fixture -def patch_diff(testrepo, new_content): +def patch_diff(testrepo: Repository, new_content: str) -> Diff: # Create the patch with (Path(testrepo.workdir) / 'hello.txt').open('wb') as f: f.write(new_content.encode('utf-8')) patch = testrepo.diff().patch + assert patch is not None # Rollback all changes testrepo.checkout('HEAD', strategy=CheckoutStrategy.FORCE) @@ -65,7 +67,7 @@ def patch_diff(testrepo, new_content): @pytest.fixture -def foreign_patch_diff(): +def foreign_patch_diff() -> Diff: patch_contents = """diff --git a/this_file_does_not_exist b/this_file_does_not_exist index 7f129fd..af431f2 100644 --- a/this_file_does_not_exist @@ -77,13 +79,15 @@ def foreign_patch_diff(): return pygit2.Diff.parse_diff(patch_contents) -def test_apply_type_error(testrepo): +def test_apply_type_error(testrepo: Repository) -> None: # Check apply type error with pytest.raises(TypeError): - testrepo.apply('HEAD') + testrepo.apply('HEAD') # type: ignore -def test_apply_diff_to_workdir(testrepo, new_content, patch_diff): +def test_apply_diff_to_workdir( + testrepo: Repository, new_content: str, patch_diff: Diff +) -> None: # Apply the patch and compare testrepo.apply(patch_diff, ApplyLocation.WORKDIR) @@ -91,7 +95,9 @@ def test_apply_diff_to_workdir(testrepo, new_content, patch_diff): assert testrepo.status_file('hello.txt') == FileStatus.WT_MODIFIED -def test_apply_diff_to_index(testrepo, old_content, patch_diff): +def test_apply_diff_to_index( + testrepo: Repository, old_content: str, patch_diff: Diff +) -> None: # Apply the patch and compare testrepo.apply(patch_diff, ApplyLocation.INDEX) @@ -99,7 +105,9 @@ def test_apply_diff_to_index(testrepo, old_content, patch_diff): assert testrepo.status_file('hello.txt') & FileStatus.INDEX_MODIFIED -def test_apply_diff_to_both(testrepo, new_content, patch_diff): +def test_apply_diff_to_both( + testrepo: Repository, new_content: str, patch_diff: Diff +) -> None: # Apply the patch and compare testrepo.apply(patch_diff, ApplyLocation.BOTH) @@ -107,7 +115,9 @@ def test_apply_diff_to_both(testrepo, new_content, patch_diff): assert testrepo.status_file('hello.txt') & FileStatus.INDEX_MODIFIED -def test_diff_applies_to_workdir(testrepo, old_content, patch_diff): +def test_diff_applies_to_workdir( + testrepo: Repository, old_content: str, patch_diff: Diff +) -> None: # See if patch applies assert testrepo.applies(patch_diff, ApplyLocation.WORKDIR) @@ -122,7 +132,9 @@ def test_diff_applies_to_workdir(testrepo, old_content, patch_diff): assert testrepo.applies(patch_diff, ApplyLocation.INDEX) -def test_diff_applies_to_index(testrepo, old_content, patch_diff): +def test_diff_applies_to_index( + testrepo: Repository, old_content: str, patch_diff: Diff +) -> None: # See if patch applies assert testrepo.applies(patch_diff, ApplyLocation.INDEX) @@ -137,7 +149,9 @@ def test_diff_applies_to_index(testrepo, old_content, patch_diff): assert testrepo.applies(patch_diff, ApplyLocation.WORKDIR) -def test_diff_applies_to_both(testrepo, old_content, patch_diff): +def test_diff_applies_to_both( + testrepo: Repository, old_content: str, patch_diff: Diff +) -> None: # See if patch applies assert testrepo.applies(patch_diff, ApplyLocation.BOTH) @@ -151,7 +165,9 @@ def test_diff_applies_to_both(testrepo, old_content, patch_diff): assert not testrepo.applies(patch_diff, ApplyLocation.INDEX) -def test_applies_error(testrepo, old_content, patch_diff, foreign_patch_diff): +def test_applies_error( + testrepo: Repository, old_content: str, patch_diff: Diff, foreign_patch_diff: Diff +) -> None: # Try to apply a "foreign" patch that affects files that aren't in the repo; # ensure we get OSError about the missing file (due to raise_error) with pytest.raises(OSError): From f40d129ea49efb746ccb3cb1d9891df78bd29dce Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Wed, 23 Jul 2025 22:40:51 +0200 Subject: [PATCH 4/9] improve types according to test/test_archive.py --- pygit2/_pygit2.pyi | 9 +++++++++ pygit2/index.py | 8 ++++---- test/test_archive.py | 10 ++++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 38c7545b..207be322 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -1,6 +1,7 @@ from typing import Iterator, Literal, Optional, overload, Type, TypedDict from io import IOBase, DEFAULT_BUFFER_SIZE from queue import Queue +import tarfile from threading import Event from . import Index from .enums import ( @@ -290,6 +291,7 @@ class Object: type: 'Literal[GIT_OBJ_COMMIT] | Literal[GIT_OBJ_TREE] | Literal[GIT_OBJ_TAG] | Literal[GIT_OBJ_BLOB]' type_str: "Literal['commit'] | Literal['tree'] | Literal['tag'] | Literal['blob']" author: Signature + committer: Signature tree: Tree @overload def peel( @@ -838,6 +840,13 @@ class Repository: def walk( self, oid: _OidArg | None, sort_mode: SortMode = SortMode.NONE ) -> Walker: ... + def write_archive( + self, + treeish: str | Tree | Object | Oid, + archive: tarfile.TarFile, + timestamp: int | None = None, + prefix: str = '', + ) -> None: ... class RevSpec: flags: int diff --git a/pygit2/index.py b/pygit2/index.py index 2238fa9d..0d485ae4 100644 --- a/pygit2/index.py +++ b/pygit2/index.py @@ -42,7 +42,7 @@ class Index: # a proper implementation in some places: e.g. checking the index type # from C code (see Tree_diff_to_index) - def __init__(self, path=None): + def __init__(self, path: str | None = None) -> None: """Create a new Index If path is supplied, the read and write methods will use that path @@ -69,13 +69,13 @@ def from_c(cls, repo, ptr): def _pointer(self): return bytes(ffi.buffer(self._cindex)[:]) - def __del__(self): + def __del__(self) -> None: C.git_index_free(self._index) - def __len__(self): + def __len__(self) -> int: return C.git_index_entrycount(self._index) - def __contains__(self, path): + def __contains__(self, path) -> bool: err = C.git_index_find(ffi.NULL, self._index, to_bytes(path)) if err == C.GIT_ENOTFOUND: return False diff --git a/test/test_archive.py b/test/test_archive.py index 7e2454f1..e1124244 100644 --- a/test/test_archive.py +++ b/test/test_archive.py @@ -26,14 +26,16 @@ from pathlib import Path import tarfile -from pygit2 import Index, Oid, Tree, Object +from pygit2 import Index, Oid, Tree, Object, Repository TREE_HASH = 'fd937514cb799514d4b81bb24c5fcfeb6472b245' COMMIT_HASH = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' -def check_writing(repo, treeish, timestamp=None): +def check_writing( + repo: Repository, treeish: str | Tree | Oid | Object, timestamp: int | None = None +) -> None: archive = tarfile.open('foo.tar', mode='w') repo.write_archive(treeish, archive) @@ -55,13 +57,13 @@ def check_writing(repo, treeish, timestamp=None): path.unlink() -def test_write_tree(testrepo): +def test_write_tree(testrepo: Repository) -> None: check_writing(testrepo, TREE_HASH) check_writing(testrepo, Oid(hex=TREE_HASH)) check_writing(testrepo, testrepo[TREE_HASH]) -def test_write_commit(testrepo): +def test_write_commit(testrepo: Repository) -> None: commit_timestamp = testrepo[COMMIT_HASH].committer.time check_writing(testrepo, COMMIT_HASH, commit_timestamp) check_writing(testrepo, Oid(hex=COMMIT_HASH), commit_timestamp) From 11f9a67664979224000446ccfc5b25256c6ecfb7 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 24 Jul 2025 21:48:10 +0200 Subject: [PATCH 5/9] imporve types accoding to test/test_attributes.py --- pygit2/_pygit2.pyi | 9 +++++++++ test/test_attributes.py | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 207be322..5f0ed970 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -1,10 +1,12 @@ from typing import Iterator, Literal, Optional, overload, Type, TypedDict from io import IOBase, DEFAULT_BUFFER_SIZE +from pathlib import Path from queue import Queue import tarfile from threading import Event from . import Index from .enums import ( + AttrCheck, ApplyLocation, BranchType, BlobFilter, @@ -793,6 +795,13 @@ class Repository: def descendant_of(self, oid1: _OidArg, oid2: _OidArg) -> bool: ... def expand_id(self, hex: str) -> Oid: ... def free(self) -> None: ... + def get_attr( + self, + path: str | bytes | Path, + name: str | bytes, + flags: AttrCheck = AttrCheck.FILE_THEN_INDEX, + commit: _OidArg | None = None, + ) -> bool | None | str: ... def git_object_lookup_prefix(self, oid: _OidArg) -> Object: ... def list_worktrees(self) -> list[str]: ... def listall_branches(self, flag: BranchType = BranchType.LOCAL) -> list[str]: ... diff --git a/test/test_attributes.py b/test/test_attributes.py index 00ac91ad..12f9106b 100644 --- a/test/test_attributes.py +++ b/test/test_attributes.py @@ -26,8 +26,10 @@ # Standard Library from pathlib import Path +from pygit2 import Repository -def test_no_attr(testrepo): + +def test_no_attr(testrepo: Repository) -> None: assert testrepo.get_attr('file', 'foo') is None with (Path(testrepo.workdir) / '.gitattributes').open('w+') as f: @@ -41,7 +43,7 @@ def test_no_attr(testrepo): assert 'lf' == testrepo.get_attr('file.sh', 'eol') -def test_no_attr_aspath(testrepo): +def test_no_attr_aspath(testrepo: Repository) -> None: with (Path(testrepo.workdir) / '.gitattributes').open('w+') as f: print('*.py text\n', file=f) From 0d7e6f9d32c6107883950f45bc94c2dbf0e7384f Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 24 Jul 2025 23:00:06 +0200 Subject: [PATCH 6/9] improve type according to test/test_blame.py this change introduces a new interface file: _pygit2_c.pyi the idea is to create python interfaces for c structs --- pygit2/_pygit2.pyi | 16 +++++++++++++- pygit2/_pygit2_c.pyi | 36 +++++++++++++++++++++++++++++++ pygit2/blame.py | 51 +++++++++++++++++++++++++++----------------- pygit2/utils.py | 19 +++++++++++++---- test/test_blame.py | 21 +++++++++--------- 5 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 pygit2/_pygit2_c.pyi diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 5f0ed970..cdd7a3db 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -10,6 +10,7 @@ from .enums import ( ApplyLocation, BranchType, BlobFilter, + BlameFlag, CheckoutStrategy, DeltaStatus, DiffFind, @@ -28,10 +29,13 @@ from .enums import ( ) from collections.abc import Generator +from ._pygit2_c import GitSignatureC, _Pointer + from .repository import BaseRepository from .submodules import SubmoduleCollection, Submodule from .remotes import Remote from .callbacks import CheckoutCallbacks +from .blame import Blame GIT_OBJ_BLOB = Literal[3] GIT_OBJ_COMMIT = Literal[1] @@ -724,6 +728,16 @@ class Repository: def apply( self, diff: Diff, location: ApplyLocation = ApplyLocation.WORKDIR ) -> None: ... + def blame( + self, + path: str, + flags: BlameFlag = BlameFlag.NORMAL, + min_match_characters: int | None = None, + newest_commit: _OidArg | None = None, + oldest_commit: _OidArg | None = None, + min_line: int | None = None, + max_line: int | None = None, + ) -> Blame: ... def checkout( self, refname: Optional[_OidArg], @@ -864,7 +878,7 @@ class RevSpec: class Signature: _encoding: str | None - _pointer: bytes + _pointer: _Pointer[GitSignatureC] email: str name: str offset: int diff --git a/pygit2/_pygit2_c.pyi b/pygit2/_pygit2_c.pyi new file mode 100644 index 00000000..c6208553 --- /dev/null +++ b/pygit2/_pygit2_c.pyi @@ -0,0 +1,36 @@ +from typing import NewType, Generic, Literal, TypeVar, overload + +T = TypeVar('T') + +char = NewType('char', object) +char_pointer = NewType('char_pointer', object) + +class _Pointer(Generic[T]): + @overload + def __getitem__(self, item: Literal[0]) -> T: ... + @overload + def __getitem__(self, item: slice[None, None, None]) -> bytes: ... + +class GitTimeC: + # incomplete + time: int + offset: int + +class GitSignatureC: + name: char_pointer + email: char_pointer + when: GitTimeC + +class GitHunkC: + # incomplete + boundary: char + final_start_line_number: int + final_signature: GitSignatureC + orig_signature: GitSignatureC + orig_start_line_number: int + orig_path: str + lines_in_hunk: int + +class GitBlameC: + # incomplete + pass diff --git a/pygit2/blame.py b/pygit2/blame.py index a1b8e42e..d364e96d 100644 --- a/pygit2/blame.py +++ b/pygit2/blame.py @@ -23,13 +23,18 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. +from typing import Iterator, TYPE_CHECKING + # Import from pygit2 from .ffi import ffi, C from .utils import GenericIterator -from ._pygit2 import Signature, Oid +from ._pygit2 import Signature, Oid, Repository + +if TYPE_CHECKING: + from ._pygit2_c import GitSignatureC, GitHunkC, GitBlameC -def wrap_signature(csig): +def wrap_signature(csig: 'GitSignatureC') -> None | Signature: if not csig: return None @@ -43,88 +48,94 @@ def wrap_signature(csig): class BlameHunk: + _blame: 'Blame' + _hunk: 'GitHunkC' + @classmethod - def _from_c(cls, blame, ptr): + def _from_c(cls, blame: 'Blame', ptr: 'GitHunkC') -> 'BlameHunk': hunk = cls.__new__(cls) hunk._blame = blame hunk._hunk = ptr return hunk @property - def lines_in_hunk(self): + def lines_in_hunk(self) -> int: """Number of lines""" return self._hunk.lines_in_hunk @property - def boundary(self): + def boundary(self) -> bool: """Tracked to a boundary commit""" # Casting directly to bool via cffi does not seem to work return int(ffi.cast('int', self._hunk.boundary)) != 0 @property - def final_start_line_number(self): + def final_start_line_number(self) -> int: """Final start line number""" return self._hunk.final_start_line_number @property - def final_committer(self): + def final_committer(self) -> None | Signature: """Final committer""" return wrap_signature(self._hunk.final_signature) @property - def final_commit_id(self): + def final_commit_id(self) -> Oid: return Oid( raw=bytes(ffi.buffer(ffi.addressof(self._hunk, 'final_commit_id'))[:]) ) @property - def orig_start_line_number(self): + def orig_start_line_number(self) -> int: """Origin start line number""" return self._hunk.orig_start_line_number @property - def orig_committer(self): + def orig_committer(self) -> None | Signature: """Original committer""" return wrap_signature(self._hunk.orig_signature) @property - def orig_commit_id(self): + def orig_commit_id(self) -> Oid: return Oid( raw=bytes(ffi.buffer(ffi.addressof(self._hunk, 'orig_commit_id'))[:]) ) @property - def orig_path(self): + def orig_path(self) -> None | str: """Original path""" path = self._hunk.orig_path if not path: return None - return ffi.string(path).decode('utf-8') + return ffi.string(path).decode('utf-8') # type: ignore[no-any-return] class Blame: + _repo: Repository + _blame: 'GitBlameC' + @classmethod - def _from_c(cls, repo, ptr): + def _from_c(cls, repo: Repository, ptr: 'GitBlameC') -> 'Blame': blame = cls.__new__(cls) blame._repo = repo blame._blame = ptr return blame - def __del__(self): + def __del__(self) -> None: C.git_blame_free(self._blame) - def __len__(self): - return C.git_blame_get_hunk_count(self._blame) + def __len__(self) -> int: + return C.git_blame_get_hunk_count(self._blame) # type: ignore[no-any-return] - def __getitem__(self, index): + def __getitem__(self, index: int) -> BlameHunk: chunk = C.git_blame_get_hunk_byindex(self._blame, index) if not chunk: raise IndexError return BlameHunk._from_c(self, chunk) - def for_line(self, line_no): + def for_line(self, line_no: int) -> BlameHunk: """ Returns the object for a given line given its number in the current Blame. @@ -143,5 +154,5 @@ def for_line(self, line_no): return BlameHunk._from_c(self, chunk) - def __iter__(self): + def __iter__(self) -> Iterator[BlameHunk]: return GenericIterator(self) diff --git a/pygit2/utils.py b/pygit2/utils.py index 1139b23c..0298b3ce 100644 --- a/pygit2/utils.py +++ b/pygit2/utils.py @@ -29,6 +29,8 @@ # Import from pygit2 from .ffi import ffi, C +from typing import Protocol, Iterator, TypeVar, Generic + def maybe_string(ptr): if not ptr: @@ -153,22 +155,31 @@ def assign_to(self, git_strarray): git_strarray.count = len(self.__strings) -class GenericIterator: +T = TypeVar('T') +U = TypeVar('U', covariant=True) + + +class SequenceProtocol(Protocol[U]): + def __len__(self) -> int: ... + def __getitem__(self, index: int) -> U: ... + + +class GenericIterator(Generic[T]): """Helper to easily implement an iterator. The constructor gets a container which must implement __len__ and __getitem__ """ - def __init__(self, container): + def __init__(self, container: SequenceProtocol[T]) -> None: self.container = container self.length = len(container) self.idx = 0 - def __iter__(self): + def __iter__(self) -> Iterator[T]: return self - def __next__(self): + def __next__(self) -> T: idx = self.idx if idx >= self.length: raise StopIteration diff --git a/test/test_blame.py b/test/test_blame.py index 251f7e6d..84b7d902 100644 --- a/test/test_blame.py +++ b/test/test_blame.py @@ -27,7 +27,7 @@ import pytest -from pygit2 import Signature, Oid +from pygit2 import Signature, Oid, Repository from pygit2.enums import BlameFlag @@ -61,7 +61,7 @@ ] -def test_blame_index(testrepo): +def test_blame_index(testrepo: Repository) -> None: blame = testrepo.blame(PATH) assert len(blame) == 3 @@ -78,7 +78,7 @@ def test_blame_index(testrepo): assert HUNKS[i][3] == hunk.boundary -def test_blame_flags(blameflagsrepo): +def test_blame_flags(blameflagsrepo: Repository) -> None: blame = blameflagsrepo.blame(PATH, flags=BlameFlag.IGNORE_WHITESPACE) assert len(blame) == 3 @@ -95,7 +95,7 @@ def test_blame_flags(blameflagsrepo): assert HUNKS[i][3] == hunk.boundary -def test_blame_with_invalid_index(testrepo): +def test_blame_with_invalid_index(testrepo: Repository) -> None: blame = testrepo.blame(PATH) def test(): @@ -106,7 +106,7 @@ def test(): test() -def test_blame_for_line(testrepo): +def test_blame_for_line(testrepo: Repository) -> None: blame = testrepo.blame(PATH) for i, line in zip(range(0, 2), range(1, 3)): @@ -123,19 +123,18 @@ def test_blame_for_line(testrepo): assert HUNKS[i][3] == hunk.boundary -def test_blame_with_invalid_line(testrepo): +def test_blame_with_invalid_line(testrepo: Repository) -> None: blame = testrepo.blame(PATH) - def test(): + with pytest.raises(IndexError): blame.for_line(0) + with pytest.raises(IndexError): blame.for_line(100000) - blame.for_line(-1) - with pytest.raises(IndexError): - test() + blame.for_line(-1) -def test_blame_newest(testrepo): +def test_blame_newest(testrepo: Repository) -> None: revs = [ ('master^2', 3), ('master^2^', 2), From 58d12711e85636a5824e452d8e9f61a46b04a8d3 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Sat, 26 Jul 2025 12:46:14 +0200 Subject: [PATCH 7/9] create type interface for _libgit2.ffi --- pygit2/__init__.py | 2 +- pygit2/_libgit2/ffi.pyi | 273 ++++++++++++++++++++++++++++++++++++++++ pygit2/_pygit2.pyi | 46 ++++--- pygit2/_pygit2_c.pyi | 36 ------ pygit2/blame.py | 4 +- pygit2/callbacks.py | 7 +- pygit2/repository.py | 11 +- pygit2/submodules.py | 5 +- pygit2/utils.py | 10 +- 9 files changed, 330 insertions(+), 64 deletions(-) create mode 100644 pygit2/_libgit2/ffi.pyi delete mode 100644 pygit2/_pygit2_c.pyi diff --git a/pygit2/__init__.py b/pygit2/__init__.py index cf83557c..6440723e 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -164,7 +164,7 @@ def clone_repository( callbacks: RemoteCallbacks | None = None, depth: int = 0, proxy: None | bool | str = None, -): +) -> Repository: """ Clones a new Git repository from *url* in the given *path*. diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi new file mode 100644 index 00000000..fdcd2928 --- /dev/null +++ b/pygit2/_libgit2/ffi.pyi @@ -0,0 +1,273 @@ +# Copyright 2010-2025 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +from typing import Literal, overload, NewType, TypeVar, Generic, Any, SupportsIndex +from typing import NewType, Literal, TypeVar, Generic, overload +from pygit2._pygit2 import Repository + +T = TypeVar('T') + +NULL_TYPE = NewType('NULL_TYPE', object) +NULL: NULL_TYPE = ... + +char = NewType('char', object) +char_pointer = NewType('char_pointer', object) + +class _Pointer(Generic[T]): + def __setitem__(self, item: Literal[0], a: T) -> None: ... + @overload + def __getitem__(self, item: Literal[0]) -> T: ... + @overload + def __getitem__(self, item: slice[None, None, None]) -> bytes: ... + +class GitTimeC: + # incomplete + time: int + offset: int + +class GitSignatureC: + name: char_pointer + email: char_pointer + when: GitTimeC + +class GitHunkC: + # incomplete + boundary: char + final_start_line_number: int + final_signature: GitSignatureC + orig_signature: GitSignatureC + orig_start_line_number: int + orig_path: char_pointer + lines_in_hunk: int + +class GitRepositoryC: + # incomplete + # TODO: this has to be unified with pygit2._pygit2(pyi).Repository + def _from_c(cls, ptr: 'GitRepositoryC', owned: bool) -> 'Repository': ... + +class GitFetchOptionsC: + # TODO: FetchOptions exist in _pygit2.pyi + # incomplete + depth: int + +class GitSubmoduleC: + pass + +class GitSubmoduleUpdateOptionsC: + fetch_opts: GitFetchOptionsC + +class UnsignedIntC: + def __getitem__(self, item: Literal[0]) -> int: ... + +class GitOidC: + id: _Pointer[bytes] + +class GitBlameOptionsC: + flags: int + min_match_characters: int + newest_commit: object + oldest_commit: object + min_line: int + max_line: int + +class GitBlameC: + # incomplete + pass + +class GitMergeOptionsC: + file_favor: int + flags: int + file_flags: int + +class GitAnnotatedCommitC: + pass + +class GitAttrOptionsC: + # incomplete + version: int + flags: int + +class GitBufC: + ptr: char_pointer + +class GitCheckoutOptionsC: + # incomplete + checkout_strategy: int + +class GitCommitC: + pass + +class GitConfigC: + pass + +class GitDescribeFormatOptionsC: + version: int + abbreviated_size: int + always_use_long_format: int + dirty_suffix: char_pointer + +class GitDescribeOptionsC: + version: int + max_candidates_tags: int + describe_strategy: int + pattern: char_pointer + only_follow_first_parent: int + show_commit_oid_as_fallback: int + +class GitDescribeResultC: + pass + +class GitIndexC: + pass + +class GitMergeFileResultC: + pass + +class GitObjectC: + pass + +class GitStashSaveOptionsC: + version: int + flags: int + stasher: GitSignatureC + message: char_pointer + paths: GitStrrayC + +class GitStrrayC: + pass + +class GitTreeC: + pass + +class GitRepositoryInitOptionsC: + version: int + flags: int + mode: int + workdir_path: char_pointer + description: char_pointer + template_path: char_pointer + initial_head: char_pointer + origin_url: char_pointer + +class GitCloneOptionsC: + pass + +class GitProxyTC: + pass + +class GitProxyOptionsC: + version: int + type: GitProxyTC + url: char_pointer + # credentials + # certificate_check + # payload + +class GitRemoteC: + pass + +class GitReferenceC: + pass + +def string(a: char_pointer) -> bytes: ... +@overload +def new(a: Literal['git_repository **']) -> _Pointer[GitRepositoryC]: ... +@overload +def new(a: Literal['git_remote **']) -> _Pointer[GitRemoteC]: ... +@overload +def new(a: Literal['git_repository_init_options *']) -> GitRepositoryInitOptionsC: ... +@overload +def new(a: Literal['git_submodule_update_options *']) -> GitSubmoduleUpdateOptionsC: ... +@overload +def new(a: Literal['git_submodule **']) -> _Pointer[GitSubmoduleC]: ... +@overload +def new(a: Literal['unsigned int *']) -> UnsignedIntC: ... +@overload +def new(a: Literal['git_proxy_options *']) -> GitProxyOptionsC: ... +@overload +def new(a: Literal['git_oid *']) -> GitOidC: ... +@overload +def new(a: Literal['git_blame **']) -> _Pointer[GitBlameC]: ... +@overload +def new(a: Literal['git_clone_options *']) -> GitCloneOptionsC: ... +@overload +def new(a: Literal['git_merge_options *']) -> GitMergeOptionsC: ... +@overload +def new(a: Literal['git_blame_options *']) -> GitBlameOptionsC: ... +@overload +def new(a: Literal['git_annotated_commit **']) -> _Pointer[GitAnnotatedCommitC]: ... +@overload +def new(a: Literal['git_attr_options *']) -> GitAttrOptionsC: ... +@overload +def new(a: Literal['git_buf *']) -> GitBufC: ... +@overload +def new(a: Literal['git_checkout_options *']) -> GitCheckoutOptionsC: ... +@overload +def new(a: Literal['git_commit **']) -> _Pointer[GitCommitC]: ... +@overload +def new(a: Literal['git_config *']) -> GitConfigC: ... +@overload +def new(a: Literal['git_describe_format_options *']) -> GitDescribeFormatOptionsC: ... +@overload +def new(a: Literal['git_describe_options *']) -> GitDescribeOptionsC: ... +@overload +def new(a: Literal['git_describe_result *']) -> GitDescribeResultC: ... +@overload +def new(a: Literal['git_describe_result **']) -> _Pointer[GitDescribeResultC]: ... +@overload +def new(a: Literal['struct git_reference **']) -> _Pointer[GitReferenceC]: ... +@overload +def new(a: Literal['git_index **']) -> _Pointer[GitIndexC]: ... +@overload +def new(a: Literal['git_merge_file_result *']) -> GitMergeFileResultC: ... +@overload +def new(a: Literal['git_object *']) -> GitObjectC: ... +@overload +def new(a: Literal['git_object **']) -> _Pointer[GitObjectC]: ... +@overload +def new(a: Literal['git_signature *']) -> GitSignatureC: ... +@overload +def new(a: Literal['git_signature **']) -> _Pointer[GitSignatureC]: ... +@overload +def new(a: Literal['git_stash_save_options *']) -> GitStashSaveOptionsC: ... +@overload +def new(a: Literal['git_tree **']) -> _Pointer[GitTreeC]: ... +@overload +def new(a: Literal['git_buf *'], b: tuple[NULL_TYPE, Literal[0]]) -> GitBufC: ... +@overload +def new(a: Literal['char **']) -> _Pointer[char_pointer]: ... +@overload +def new(a: Literal['char[]', 'char []'], b: bytes | NULL_TYPE) -> char_pointer: ... +def addressof(a: object, attribute: str) -> _Pointer[object]: ... + +class buffer(bytes): + def __init__(self, a: object) -> None: ... + def __setitem__(self, item: slice[None, None, None], value: bytes) -> None: ... + @overload + def __getitem__(self, item: SupportsIndex) -> int: ... + @overload + def __getitem__(self, item: slice[Any, Any, Any]) -> bytes: ... + +def cast(a: Literal['int'], b: object) -> int: ... diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index cdd7a3db..48c362db 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -1,4 +1,13 @@ -from typing import Iterator, Literal, Optional, overload, Type, TypedDict +from typing import ( + Iterator, + Literal, + Optional, + overload, + Type, + TypedDict, + TypeVar, + Generic, +) from io import IOBase, DEFAULT_BUFFER_SIZE from pathlib import Path from queue import Queue @@ -29,7 +38,14 @@ from .enums import ( ) from collections.abc import Generator -from ._pygit2_c import GitSignatureC, _Pointer +from ._libgit2.ffi import ( + _Pointer, + GitObjectC, + GitCommitC, + GitRepositoryC, + GitProxyOptionsC, + GitSignatureC, +) from .repository import BaseRepository from .submodules import SubmoduleCollection, Submodule @@ -287,8 +303,10 @@ GIT_FILTER_NO_SYSTEM_ATTRIBUTES: int GIT_FILTER_ATTRIBUTES_FROM_HEAD: int GIT_FILTER_ATTRIBUTES_FROM_COMMIT: int -class Object: - _pointer: bytes +T = TypeVar('T') + +class _ObjectBase(Generic[T]): + _pointer: _Pointer[T] filemode: FileMode id: Oid name: str | None @@ -320,6 +338,9 @@ class Object: def __lt__(self, other) -> bool: ... def __ne__(self, other) -> bool: ... +class Object(_ObjectBase[GitObjectC]): + pass + class Reference: name: str raw_name: bytes @@ -395,7 +416,7 @@ class Branch(Reference): class FetchOptions: # incomplete depth: int - proxy_opts: ProxyOpts + proxy_opts: GitProxyOptionsC class CloneOptions: # incomplete @@ -410,7 +431,8 @@ class CloneOptions: remote_cb: object remote_cb_payload: object -class Commit(Object): +class Commit(_ObjectBase[GitCommitC]): + _pointer: _Pointer[GitCommitC] author: Signature commit_time: int commit_time_offset: int @@ -640,16 +662,11 @@ class _StrArray: # incomplete count: int -class ProxyOpts: - # incomplete - type: object - url: str - class PushOptions: version: int pb_parallelism: int callbacks: object # TODO - proxy_opts: ProxyOpts + proxy_opts: GitProxyOptionsC follow_redirects: object # TODO custom_headers: _StrArray remote_push_options: _StrArray @@ -696,7 +713,7 @@ class Branches: def __contains__(self, name: _OidArg) -> bool: ... class Repository: - _pointer: bytes + _pointer: GitRepositoryC default_signature: Signature head: Reference head_is_detached: bool @@ -716,7 +733,8 @@ class Repository: def __init__(self, *args, **kwargs) -> None: ... def TreeBuilder(self, src: Tree | _OidArg = ...) -> TreeBuilder: ... def _disown(self, *args, **kwargs) -> None: ... - def _from_c(self, *args, **kwargs) -> 'Repository': ... + @classmethod + def _from_c(cls, ptr: 'GitRepositoryC', owned: bool) -> 'Repository': ... def __getitem__(self, key: str | Oid) -> Object: ... def add_worktree(self, name: str, path: str, ref: Reference = ...) -> Worktree: ... def applies( diff --git a/pygit2/_pygit2_c.pyi b/pygit2/_pygit2_c.pyi deleted file mode 100644 index c6208553..00000000 --- a/pygit2/_pygit2_c.pyi +++ /dev/null @@ -1,36 +0,0 @@ -from typing import NewType, Generic, Literal, TypeVar, overload - -T = TypeVar('T') - -char = NewType('char', object) -char_pointer = NewType('char_pointer', object) - -class _Pointer(Generic[T]): - @overload - def __getitem__(self, item: Literal[0]) -> T: ... - @overload - def __getitem__(self, item: slice[None, None, None]) -> bytes: ... - -class GitTimeC: - # incomplete - time: int - offset: int - -class GitSignatureC: - name: char_pointer - email: char_pointer - when: GitTimeC - -class GitHunkC: - # incomplete - boundary: char - final_start_line_number: int - final_signature: GitSignatureC - orig_signature: GitSignatureC - orig_start_line_number: int - orig_path: str - lines_in_hunk: int - -class GitBlameC: - # incomplete - pass diff --git a/pygit2/blame.py b/pygit2/blame.py index d364e96d..66d51354 100644 --- a/pygit2/blame.py +++ b/pygit2/blame.py @@ -31,7 +31,7 @@ from ._pygit2 import Signature, Oid, Repository if TYPE_CHECKING: - from ._pygit2_c import GitSignatureC, GitHunkC, GitBlameC + from ._libgit2.ffi import GitSignatureC, GitHunkC, GitBlameC def wrap_signature(csig: 'GitSignatureC') -> None | Signature: @@ -108,7 +108,7 @@ def orig_path(self) -> None | str: if not path: return None - return ffi.string(path).decode('utf-8') # type: ignore[no-any-return] + return ffi.string(path).decode('utf-8') class Blame: diff --git a/pygit2/callbacks.py b/pygit2/callbacks.py index 60f14f1c..af141aaf 100644 --- a/pygit2/callbacks.py +++ b/pygit2/callbacks.py @@ -79,7 +79,8 @@ if TYPE_CHECKING: from .remotes import TransferProgress - from ._pygit2 import ProxyOpts, PushOptions, CloneOptions + from ._pygit2 import PushOptions, CloneOptions + from pygit2._libgit2.ffi import GitProxyOptionsC # # The payload is the way to pass information from the pygit2 API, through # libgit2, to the Python callbacks. And back. @@ -390,9 +391,9 @@ def git_fetch_options(payload, opts=None): @contextmanager def git_proxy_options( payload: object, - opts: Optional['ProxyOpts'] = None, + opts: Optional['GitProxyOptionsC'] = None, proxy: None | bool | str = None, -) -> Generator['ProxyOpts', None, None]: +) -> Generator['GitProxyOptionsC', None, None]: if opts is None: opts = ffi.new('git_proxy_options *') C.git_proxy_options_init(opts, C.GIT_PROXY_OPTIONS_VERSION) diff --git a/pygit2/repository.py b/pygit2/repository.py index 4c2cf26f..2a285691 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -29,7 +29,7 @@ from time import time import tarfile import typing -from typing import Optional +from typing import Optional, TYPE_CHECKING # Import from pygit2 from ._pygit2 import Repository as _Repository, init_file_backend @@ -64,6 +64,9 @@ from .submodules import SubmoduleCollection from .utils import to_bytes, StrArray +if TYPE_CHECKING: + from pygit2._libgit2.ffi import GitRepositoryC + class BaseRepository(_Repository): def __init__(self, *args, **kwargs): @@ -167,6 +170,7 @@ def hashfile( """ c_path = to_bytes(path) + c_as_path: ffi.NULL_TYPE | bytes if as_path is None: c_as_path = ffi.NULL else: @@ -615,6 +619,7 @@ def blame( """ options = ffi.new('git_blame_options *') + C.git_blame_options_init(options, C.GIT_BLAME_OPTIONS_VERSION) if flags: options.flags = int(flags) @@ -1658,10 +1663,10 @@ def __init__( super().__init__() @classmethod - def _from_c(cls, ptr, owned): + def _from_c(cls, ptr: 'GitRepositoryC', owned: bool) -> 'Repository': cptr = ffi.new('git_repository **') cptr[0] = ptr repo = cls.__new__(cls) - BaseRepository._from_c(repo, bytes(ffi.buffer(cptr)[:]), owned) + BaseRepository._from_c(repo, bytes(ffi.buffer(cptr)[:]), owned) # type: ignore repo._common_init() return repo diff --git a/pygit2/submodules.py b/pygit2/submodules.py index 2aec25ec..4d621a5f 100644 --- a/pygit2/submodules.py +++ b/pygit2/submodules.py @@ -38,14 +38,15 @@ if TYPE_CHECKING: from .repository import BaseRepository from pygit2 import Repository + from pygit2._libgit2.ffi import GitSubmoduleC class Submodule: _repo: BaseRepository - _subm: object + _subm: 'GitSubmoduleC' @classmethod - def _from_c(cls, repo: BaseRepository, cptr): + def _from_c(cls, repo: BaseRepository, cptr: 'GitSubmoduleC') -> 'Submodule': subm = cls.__new__(cls) subm._repo = repo diff --git a/pygit2/utils.py b/pygit2/utils.py index 0298b3ce..430b6445 100644 --- a/pygit2/utils.py +++ b/pygit2/utils.py @@ -29,7 +29,7 @@ # Import from pygit2 from .ffi import ffi, C -from typing import Protocol, Iterator, TypeVar, Generic +from typing import Protocol, Iterator, TypeVar, Generic, Union def maybe_string(ptr): @@ -39,7 +39,11 @@ def maybe_string(ptr): return ffi.string(ptr).decode('utf8', errors='surrogateescape') -def to_bytes(s, encoding='utf-8', errors='strict'): +def to_bytes( + s: Union[str, bytes, 'ffi.NULL_TYPE', os.PathLike[str], None], + encoding: str = 'utf-8', + errors: str = 'strict', +) -> Union[bytes, 'ffi.NULL_TYPE']: if s == ffi.NULL or s is None: return ffi.NULL @@ -49,7 +53,7 @@ def to_bytes(s, encoding='utf-8', errors='strict'): if isinstance(s, bytes): return s - return s.encode(encoding, errors) + return s.encode(encoding, errors) # type: ignore[union-attr] def to_str(s): From 3955534c12080166956687598f545101332cbca4 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Sat, 26 Jul 2025 13:41:19 +0200 Subject: [PATCH 8/9] sort imports with "ruff check --select I --fix" --- pygit2/__init__.py | 20 +++++++------- pygit2/_libgit2/ffi.pyi | 4 +-- pygit2/_pygit2.pyi | 49 +++++++++++++++++----------------- pygit2/_run.py | 2 +- pygit2/blame.py | 9 ++++--- pygit2/blob.py | 2 +- pygit2/branches.py | 3 ++- pygit2/callbacks.py | 17 ++++++------ pygit2/config.py | 2 +- pygit2/credentials.py | 1 - pygit2/errors.py | 3 +-- pygit2/ffi.py | 3 ++- pygit2/index.py | 7 +++-- pygit2/packbuilder.py | 2 +- pygit2/references.py | 1 + pygit2/refspec.py | 2 +- pygit2/remotes.py | 10 ++++--- pygit2/repository.py | 30 ++++++++++++++------- pygit2/submodules.py | 12 +++++---- pygit2/utils.py | 5 ++-- setup.py | 12 ++++----- test/conftest.py | 4 ++- test/test_apply_diff.py | 11 ++++---- test/test_archive.py | 5 ++-- test/test_blame.py | 3 +-- test/test_blob.py | 4 +-- test/test_branch.py | 9 ++++--- test/test_branch_empty.py | 2 +- test/test_cherrypick.py | 1 + test/test_commit.py | 4 +-- test/test_commit_trailer.py | 3 ++- test/test_config.py | 2 +- test/test_credentials.py | 6 ++--- test/test_describe.py | 2 +- test/test_diff.py | 3 +-- test/test_filter.py | 3 ++- test/test_index.py | 3 ++- test/test_mailmap.py | 1 - test/test_merge.py | 2 +- test/test_nonunicode.py | 2 +- test/test_note.py | 2 +- test/test_object.py | 3 +-- test/test_odb.py | 2 +- test/test_odb_backend.py | 2 +- test/test_oid.py | 2 +- test/test_packbuilder.py | 1 + test/test_patch.py | 2 +- test/test_patch_encoding.py | 1 - test/test_refdb_backend.py | 3 ++- test/test_refs.py | 13 ++++++--- test/test_remote.py | 6 ++--- test/test_remote_utf8.py | 4 ++- test/test_repository.py | 14 +++++++--- test/test_repository_bare.py | 2 +- test/test_repository_custom.py | 1 + test/test_revparse.py | 3 ++- test/test_revwalk.py | 1 - test/test_submodule.py | 7 ++--- test/test_tag.py | 1 - test/test_tree.py | 2 +- test/utils.py | 5 ++-- 61 files changed, 189 insertions(+), 154 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 6440723e..f97daa87 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -30,26 +30,29 @@ import os import typing -# Low level API -from ._pygit2 import * -from ._pygit2 import _cache_enums - # High level API from . import enums from ._build import __version__ + +# Low level API +from ._pygit2 import * +from ._pygit2 import _cache_enums from .blame import Blame, BlameHunk from .blob import BlobIO -from .callbacks import Payload, RemoteCallbacks, CheckoutCallbacks, StashApplyCallbacks from .callbacks import ( + CheckoutCallbacks, + Payload, + RemoteCallbacks, + StashApplyCallbacks, + get_credentials, git_clone_options, git_fetch_options, git_proxy_options, - get_credentials, ) from .config import Config from .credentials import * -from .errors import check_error, Passthrough -from .ffi import ffi, C +from .errors import Passthrough, check_error +from .ffi import C, ffi from .filter import Filter from .index import Index, IndexEntry from .legacyenums import * @@ -60,7 +63,6 @@ from .submodules import Submodule from .utils import to_bytes, to_str - # Features features = enums.Feature(C.git_libgit2_features()) diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index fdcd2928..39b36b84 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -23,8 +23,8 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -from typing import Literal, overload, NewType, TypeVar, Generic, Any, SupportsIndex -from typing import NewType, Literal, TypeVar, Generic, overload +from typing import Any, Generic, Literal, NewType, SupportsIndex, TypeVar, overload + from pygit2._pygit2 import Repository T = TypeVar('T') diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 48c362db..29c99084 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -1,25 +1,37 @@ +import tarfile +from collections.abc import Generator +from io import DEFAULT_BUFFER_SIZE, IOBase +from pathlib import Path +from queue import Queue +from threading import Event from typing import ( + Generic, Iterator, Literal, Optional, - overload, Type, TypedDict, TypeVar, - Generic, + overload, ) -from io import IOBase, DEFAULT_BUFFER_SIZE -from pathlib import Path -from queue import Queue -import tarfile -from threading import Event + from . import Index +from ._libgit2.ffi import ( + GitCommitC, + GitObjectC, + GitProxyOptionsC, + GitRepositoryC, + GitSignatureC, + _Pointer, +) +from .blame import Blame +from .callbacks import CheckoutCallbacks from .enums import ( - AttrCheck, ApplyLocation, - BranchType, - BlobFilter, + AttrCheck, BlameFlag, + BlobFilter, + BranchType, CheckoutStrategy, DeltaStatus, DiffFind, @@ -36,22 +48,9 @@ from .enums import ( ResetMode, SortMode, ) -from collections.abc import Generator - -from ._libgit2.ffi import ( - _Pointer, - GitObjectC, - GitCommitC, - GitRepositoryC, - GitProxyOptionsC, - GitSignatureC, -) - -from .repository import BaseRepository -from .submodules import SubmoduleCollection, Submodule from .remotes import Remote -from .callbacks import CheckoutCallbacks -from .blame import Blame +from .repository import BaseRepository +from .submodules import Submodule, SubmoduleCollection GIT_OBJ_BLOB = Literal[3] GIT_OBJ_COMMIT = Literal[1] diff --git a/pygit2/_run.py b/pygit2/_run.py index ea921d91..bb8238c9 100644 --- a/pygit2/_run.py +++ b/pygit2/_run.py @@ -29,8 +29,8 @@ # Import from the Standard Library import codecs -from pathlib import Path import sys +from pathlib import Path # Import from cffi from cffi import FFI # type: ignore diff --git a/pygit2/blame.py b/pygit2/blame.py index 66d51354..c5e982b3 100644 --- a/pygit2/blame.py +++ b/pygit2/blame.py @@ -23,15 +23,16 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -from typing import Iterator, TYPE_CHECKING +from typing import TYPE_CHECKING, Iterator + +from ._pygit2 import Oid, Repository, Signature # Import from pygit2 -from .ffi import ffi, C +from .ffi import C, ffi from .utils import GenericIterator -from ._pygit2 import Signature, Oid, Repository if TYPE_CHECKING: - from ._libgit2.ffi import GitSignatureC, GitHunkC, GitBlameC + from ._libgit2.ffi import GitBlameC, GitHunkC, GitSignatureC def wrap_signature(csig: 'GitSignatureC') -> None | Signature: diff --git a/pygit2/blob.py b/pygit2/blob.py index 23e8a9da..1232fb2f 100644 --- a/pygit2/blob.py +++ b/pygit2/blob.py @@ -2,8 +2,8 @@ import threading import time from contextlib import AbstractContextManager -from typing import Optional from queue import Queue +from typing import Optional from ._pygit2 import Blob, Oid from .enums import BlobFilter diff --git a/pygit2/branches.py b/pygit2/branches.py index c6323a1f..9f5ac6f1 100644 --- a/pygit2/branches.py +++ b/pygit2/branches.py @@ -24,10 +24,11 @@ # Boston, MA 02110-1301, USA. from __future__ import annotations + from typing import TYPE_CHECKING -from .enums import BranchType, ReferenceType from ._pygit2 import Commit, Oid +from .enums import BranchType, ReferenceType # Need BaseRepository for type hints, but don't let it cause a circular dependency if TYPE_CHECKING: diff --git a/pygit2/callbacks.py b/pygit2/callbacks.py index af141aaf..e2b861d9 100644 --- a/pygit2/callbacks.py +++ b/pygit2/callbacks.py @@ -65,22 +65,23 @@ # Standard Library from contextlib import contextmanager from functools import wraps -from typing import Optional, Union, TYPE_CHECKING, Callable, Generator +from typing import TYPE_CHECKING, Callable, Generator, Optional, Union # pygit2 -from ._pygit2 import Oid, DiffFile +from ._pygit2 import DiffFile, Oid +from .credentials import Keypair, Username, UserPass from .enums import CheckoutNotify, CheckoutStrategy, CredentialType, StashApplyProgress -from .errors import check_error, Passthrough -from .ffi import ffi, C -from .utils import maybe_string, to_bytes, ptr_to_bytes, StrArray -from .credentials import Username, UserPass, Keypair +from .errors import Passthrough, check_error +from .ffi import C, ffi +from .utils import StrArray, maybe_string, ptr_to_bytes, to_bytes _Credentials = Username | UserPass | Keypair if TYPE_CHECKING: - from .remotes import TransferProgress - from ._pygit2 import PushOptions, CloneOptions from pygit2._libgit2.ffi import GitProxyOptionsC + + from ._pygit2 import CloneOptions, PushOptions + from .remotes import TransferProgress # # The payload is the way to pass information from the pygit2 API, through # libgit2, to the Python callbacks. And back. diff --git a/pygit2/config.py b/pygit2/config.py index cba72911..e8619eda 100644 --- a/pygit2/config.py +++ b/pygit2/config.py @@ -30,7 +30,7 @@ # Import from pygit2 from .errors import check_error -from .ffi import ffi, C +from .ffi import C, ffi from .utils import to_bytes diff --git a/pygit2/credentials.py b/pygit2/credentials.py index 2b307f94..52edc8ce 100644 --- a/pygit2/credentials.py +++ b/pygit2/credentials.py @@ -29,7 +29,6 @@ from .enums import CredentialType - if TYPE_CHECKING: from pathlib import Path diff --git a/pygit2/errors.py b/pygit2/errors.py index 3ecef9df..8bfa8ff5 100644 --- a/pygit2/errors.py +++ b/pygit2/errors.py @@ -24,9 +24,8 @@ # Boston, MA 02110-1301, USA. # Import from pygit2 -from .ffi import ffi, C from ._pygit2 import GitError - +from .ffi import C, ffi value_errors = set([C.GIT_EEXISTS, C.GIT_EINVALIDSPEC, C.GIT_EAMBIGUOUS]) diff --git a/pygit2/ffi.py b/pygit2/ffi.py index a9a0c615..6fe791a5 100644 --- a/pygit2/ffi.py +++ b/pygit2/ffi.py @@ -24,4 +24,5 @@ # Boston, MA 02110-1301, USA. # Import from pygit2 -from ._libgit2 import ffi, lib as C # type: ignore # noqa: F401 +from ._libgit2 import ffi # type: ignore # noqa: F401 +from ._libgit2 import lib as C diff --git a/pygit2/index.py b/pygit2/index.py index 0d485ae4..e6ae6593 100644 --- a/pygit2/index.py +++ b/pygit2/index.py @@ -28,12 +28,11 @@ from dataclasses import dataclass # Import from pygit2 -from ._pygit2 import Oid, Tree, Diff +from ._pygit2 import Diff, Oid, Tree from .enums import DiffOption, FileMode from .errors import check_error -from .ffi import ffi, C -from .utils import to_bytes, to_str -from .utils import GenericIterator, StrArray +from .ffi import C, ffi +from .utils import GenericIterator, StrArray, to_bytes, to_str class Index: diff --git a/pygit2/packbuilder.py b/pygit2/packbuilder.py index b9844d52..5551e1fd 100644 --- a/pygit2/packbuilder.py +++ b/pygit2/packbuilder.py @@ -26,7 +26,7 @@ # Import from pygit2 from .errors import check_error -from .ffi import ffi, C +from .ffi import C, ffi from .utils import to_bytes diff --git a/pygit2/references.py b/pygit2/references.py index ca1d23dc..630c3fe5 100644 --- a/pygit2/references.py +++ b/pygit2/references.py @@ -24,6 +24,7 @@ # Boston, MA 02110-1301, USA. from __future__ import annotations + from typing import TYPE_CHECKING from .enums import ReferenceFilter diff --git a/pygit2/refspec.py b/pygit2/refspec.py index 447cf7dc..70b0f499 100644 --- a/pygit2/refspec.py +++ b/pygit2/refspec.py @@ -25,7 +25,7 @@ # Import from pygit2 from .errors import check_error -from .ffi import ffi, C +from .ffi import C, ffi from .utils import to_bytes diff --git a/pygit2/remotes.py b/pygit2/remotes.py index 7e91f6ae..195d4822 100644 --- a/pygit2/remotes.py +++ b/pygit2/remotes.py @@ -24,22 +24,24 @@ # Boston, MA 02110-1301, USA. from __future__ import annotations + from typing import TYPE_CHECKING, Any +from . import utils + # Import from pygit2 from ._pygit2 import Oid from .callbacks import ( git_fetch_options, - git_push_options, git_proxy_options, + git_push_options, git_remote_callbacks, ) from .enums import FetchPrune from .errors import check_error -from .ffi import ffi, C +from .ffi import C, ffi from .refspec import Refspec -from . import utils -from .utils import maybe_string, to_bytes, strarray_to_strings, StrArray +from .utils import StrArray, maybe_string, strarray_to_strings, to_bytes # Need BaseRepository for type hints, but don't let it cause a circular dependency if TYPE_CHECKING: diff --git a/pygit2/repository.py b/pygit2/repository.py index 2a285691..0c2db04e 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -22,21 +22,31 @@ # along with this program; see the file COPYING. If not, write to # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. +import tarfile +import typing import warnings from io import BytesIO from os import PathLike from string import hexdigits from time import time -import tarfile -import typing -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional + +from ._pygit2 import ( + GIT_OID_HEXSZ, + GIT_OID_MINPREFIXLEN, + Blob, + Commit, + InvalidSpecError, + Object, + Oid, + Reference, + Signature, + Tree, + init_file_backend, +) # Import from pygit2 -from ._pygit2 import Repository as _Repository, init_file_backend -from ._pygit2 import Oid, GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN, Object -from ._pygit2 import Reference, Tree, Commit, Blob, Signature -from ._pygit2 import InvalidSpecError - +from ._pygit2 import Repository as _Repository from .blame import Blame from .branches import Branches from .callbacks import git_checkout_options, git_stash_apply_options @@ -56,13 +66,13 @@ RepositoryState, ) from .errors import check_error -from .ffi import ffi, C +from .ffi import C, ffi from .index import Index, IndexEntry, MergeFileResult from .packbuilder import PackBuilder from .references import References from .remotes import RemoteCollection from .submodules import SubmoduleCollection -from .utils import to_bytes, StrArray +from .utils import StrArray, to_bytes if TYPE_CHECKING: from pygit2._libgit2.ffi import GitRepositoryC diff --git a/pygit2/submodules.py b/pygit2/submodules.py index 4d621a5f..0f98e4f4 100644 --- a/pygit2/submodules.py +++ b/pygit2/submodules.py @@ -24,22 +24,24 @@ # Boston, MA 02110-1301, USA. from __future__ import annotations -from typing import TYPE_CHECKING, Iterable, Iterator, Optional, Union + from pathlib import Path +from typing import TYPE_CHECKING, Iterable, Iterator, Optional, Union from ._pygit2 import Oid -from .callbacks import git_fetch_options, RemoteCallbacks +from .callbacks import RemoteCallbacks, git_fetch_options from .enums import SubmoduleIgnore, SubmoduleStatus from .errors import check_error -from .ffi import ffi, C -from .utils import to_bytes, maybe_string +from .ffi import C, ffi +from .utils import maybe_string, to_bytes # Need BaseRepository for type hints, but don't let it cause a circular dependency if TYPE_CHECKING: - from .repository import BaseRepository from pygit2 import Repository from pygit2._libgit2.ffi import GitSubmoduleC + from .repository import BaseRepository + class Submodule: _repo: BaseRepository diff --git a/pygit2/utils.py b/pygit2/utils.py index 430b6445..42798684 100644 --- a/pygit2/utils.py +++ b/pygit2/utils.py @@ -25,11 +25,10 @@ import contextlib import os +from typing import Generic, Iterator, Protocol, TypeVar, Union # Import from pygit2 -from .ffi import ffi, C - -from typing import Protocol, Iterator, TypeVar, Generic, Union +from .ffi import C, ffi def maybe_string(ptr): diff --git a/setup.py b/setup.py index 88d1536d..1ad61821 100644 --- a/setup.py +++ b/setup.py @@ -24,15 +24,15 @@ # Boston, MA 02110-1301, USA. # Import setuptools before distutils to avoid user warning -from setuptools import setup, Extension - +import os +import sys +from distutils import log from distutils.command.build import build from distutils.command.sdist import sdist -from distutils import log -import os from pathlib import Path -from subprocess import Popen, PIPE -import sys +from subprocess import PIPE, Popen + +from setuptools import Extension, setup # Import stuff from pygit2/_utils.py without loading the whole pygit2 package sys.path.insert(0, 'pygit2') diff --git a/test/conftest.py b/test/conftest.py index 1c6d7b8f..6feb9d23 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,8 +1,10 @@ -from pathlib import Path import platform +from pathlib import Path import pytest + import pygit2 + from . import utils diff --git a/test/test_apply_diff.py b/test/test_apply_diff.py index e5039324..9277d7eb 100644 --- a/test/test_apply_diff.py +++ b/test/test_apply_diff.py @@ -23,14 +23,15 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -import pygit2 -from pygit2 import Repository, Diff -from pygit2.enums import ApplyLocation, CheckoutStrategy, FileStatus -import pytest - import os from pathlib import Path +import pytest + +import pygit2 +from pygit2 import Diff, Repository +from pygit2.enums import ApplyLocation, CheckoutStrategy, FileStatus + def read_content(testrepo: Repository) -> str: with (Path(testrepo.workdir) / 'hello.txt').open('rb') as f: diff --git a/test/test_archive.py b/test/test_archive.py index e1124244..bd8ef864 100644 --- a/test/test_archive.py +++ b/test/test_archive.py @@ -23,11 +23,10 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -from pathlib import Path import tarfile +from pathlib import Path -from pygit2 import Index, Oid, Tree, Object, Repository - +from pygit2 import Index, Object, Oid, Repository, Tree TREE_HASH = 'fd937514cb799514d4b81bb24c5fcfeb6472b245' COMMIT_HASH = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' diff --git a/test/test_blame.py b/test/test_blame.py index 84b7d902..4f1d8e2b 100644 --- a/test/test_blame.py +++ b/test/test_blame.py @@ -27,10 +27,9 @@ import pytest -from pygit2 import Signature, Oid, Repository +from pygit2 import Oid, Repository, Signature from pygit2.enums import BlameFlag - PATH = 'hello.txt' HUNKS = [ diff --git a/test/test_blob.py b/test/test_blob.py index c9025f49..63e97c00 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -27,15 +27,15 @@ import io from pathlib import Path -from threading import Event from queue import Queue +from threading import Event import pytest import pygit2 from pygit2.enums import ObjectType -from . import utils +from . import utils BLOB_SHA = 'a520c24d85fbfc815d385957eed41406ca5a860b' BLOB_CONTENT = """hello world diff --git a/test/test_branch.py b/test/test_branch.py index 1128a1b1..3cb3316d 100644 --- a/test/test_branch.py +++ b/test/test_branch.py @@ -25,12 +25,13 @@ """Tests for branch methods.""" -import pygit2 -import pytest import os -from pygit2.enums import BranchType -from pygit2 import Repository +import pytest + +import pygit2 +from pygit2 import Repository +from pygit2.enums import BranchType LAST_COMMIT = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' I18N_LAST_COMMIT = '5470a671a80ac3789f1a6a8cefbcf43ce7af0563' diff --git a/test/test_branch_empty.py b/test/test_branch_empty.py index c1d07970..56198535 100644 --- a/test/test_branch_empty.py +++ b/test/test_branch_empty.py @@ -24,8 +24,8 @@ # Boston, MA 02110-1301, USA. import pytest -from pygit2.enums import BranchType +from pygit2.enums import BranchType ORIGIN_MASTER_COMMIT = '784855caf26449a1914d2cf62d12b9374d76ae78' diff --git a/test/test_cherrypick.py b/test/test_cherrypick.py index 136e7d26..d1aebbe1 100644 --- a/test/test_cherrypick.py +++ b/test/test_cherrypick.py @@ -26,6 +26,7 @@ """Tests for merging and information about it.""" from pathlib import Path + import pytest import pygit2 diff --git a/test/test_commit.py b/test/test_commit.py index 8967e5cd..a38559da 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -29,10 +29,10 @@ import pytest -from pygit2 import Signature, Oid, GitError +from pygit2 import GitError, Oid, Signature from pygit2.enums import ObjectType -from . import utils +from . import utils COMMIT_SHA = '5fe808e8953c12735680c257f56600cb0de44b10' COMMIT_SHA_TO_AMEND = ( diff --git a/test/test_commit_trailer.py b/test/test_commit_trailer.py index d7236cd8..05504759 100644 --- a/test/test_commit_trailer.py +++ b/test/test_commit_trailer.py @@ -23,9 +23,10 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -import pygit2 import pytest +import pygit2 + from . import utils diff --git a/test/test_config.py b/test/test_config.py index 0284d76f..71b2b534 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -28,8 +28,8 @@ import pytest from pygit2 import Config -from . import utils +from . import utils CONFIG_FILENAME = 'test_config' diff --git a/test/test_credentials.py b/test/test_credentials.py index e9578b36..2cfe4822 100644 --- a/test/test_credentials.py +++ b/test/test_credentials.py @@ -23,16 +23,16 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -from pathlib import Path import platform +from pathlib import Path import pytest import pygit2 -from pygit2 import Username, UserPass, Keypair, KeypairFromAgent, KeypairFromMemory +from pygit2 import Keypair, KeypairFromAgent, KeypairFromMemory, Username, UserPass from pygit2.enums import CredentialType -from . import utils +from . import utils REMOTE_NAME = 'origin' REMOTE_URL = 'git://github.com/libgit2/pygit2.git' diff --git a/test/test_describe.py b/test/test_describe.py index 22650a5d..f24c60a9 100644 --- a/test/test_describe.py +++ b/test/test_describe.py @@ -27,8 +27,8 @@ import pytest -from pygit2.enums import DescribeStrategy, ObjectType import pygit2 +from pygit2.enums import DescribeStrategy, ObjectType def add_tag(repo, name, target): diff --git a/test/test_diff.py b/test/test_diff.py index f73a4c64..5deb3481 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -25,15 +25,14 @@ """Tests for Diff objects.""" -from itertools import chain import textwrap +from itertools import chain import pytest import pygit2 from pygit2.enums import DeltaStatus, DiffFlag, DiffOption, DiffStatsFormat, FileMode - COMMIT_SHA1_1 = '5fe808e8953c12735680c257f56600cb0de44b10' COMMIT_SHA1_2 = 'c2792cfa289ae6321ecf2cd5806c2194b0fd070c' COMMIT_SHA1_3 = '2cdae28389c059815e951d0bb9eed6533f61a46b' diff --git a/test/test_filter.py b/test/test_filter.py index f37f9e1c..5fedb22b 100644 --- a/test/test_filter.py +++ b/test/test_filter.py @@ -1,5 +1,6 @@ -from io import BytesIO import codecs +from io import BytesIO + import pytest import pygit2 diff --git a/test/test_index.py b/test/test_index.py index 0fb0586f..b1fccba9 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -30,8 +30,9 @@ import pytest import pygit2 -from pygit2 import Repository, Index, Oid, IndexEntry +from pygit2 import Index, IndexEntry, Oid, Repository from pygit2.enums import FileMode + from . import utils diff --git a/test/test_mailmap.py b/test/test_mailmap.py index 3cdef056..e5a3b90e 100644 --- a/test/test_mailmap.py +++ b/test/test_mailmap.py @@ -27,7 +27,6 @@ from pygit2 import Mailmap - TEST_MAILMAP = """\ # Simple Comment line diff --git a/test/test_merge.py b/test/test_merge.py index 959854b3..538070f0 100644 --- a/test/test_merge.py +++ b/test/test_merge.py @@ -30,7 +30,7 @@ import pytest import pygit2 -from pygit2.enums import FileStatus, MergeAnalysis, MergeFavor, MergeFlag, MergeFileFlag +from pygit2.enums import FileStatus, MergeAnalysis, MergeFavor, MergeFileFlag, MergeFlag @pytest.mark.parametrize('id', [None, 42]) diff --git a/test/test_nonunicode.py b/test/test_nonunicode.py index 26b44680..3daef1a2 100644 --- a/test/test_nonunicode.py +++ b/test/test_nonunicode.py @@ -32,8 +32,8 @@ import pytest import pygit2 -from . import utils +from . import utils # FIXME Detect the filesystem rather than the operating system works_in_linux = pytest.mark.xfail( diff --git a/test/test_note.py b/test/test_note.py index 2a171924..1f95e2a4 100644 --- a/test/test_note.py +++ b/test/test_note.py @@ -25,9 +25,9 @@ """Tests for note objects.""" -from pygit2 import Signature import pytest +from pygit2 import Signature NOTE = ('6c8980ba963cad8b25a9bcaf68d4023ee57370d8', 'note message') diff --git a/test/test_object.py b/test/test_object.py index 668d2d66..95b53458 100644 --- a/test/test_object.py +++ b/test/test_object.py @@ -27,10 +27,9 @@ import pytest -from pygit2 import Tree, Tag +from pygit2 import Tag, Tree from pygit2.enums import ObjectType - BLOB_SHA = 'a520c24d85fbfc815d385957eed41406ca5a860b' BLOB_CONTENT = """hello world hola mundo diff --git a/test/test_odb.py b/test/test_odb.py index c7e60f22..ce07c937 100644 --- a/test/test_odb.py +++ b/test/test_odb.py @@ -34,8 +34,8 @@ # pygit2 from pygit2 import Odb, Oid from pygit2.enums import ObjectType -from . import utils +from . import utils BLOB_HEX = 'af431f20fc541ed6d5afede3e2dc7160f6f01f16' BLOB_RAW = binascii.unhexlify(BLOB_HEX.encode('ascii')) diff --git a/test/test_odb_backend.py b/test/test_odb_backend.py index 026834c3..c1995f26 100644 --- a/test/test_odb_backend.py +++ b/test/test_odb_backend.py @@ -34,8 +34,8 @@ # pygit2 import pygit2 from pygit2.enums import ObjectType -from . import utils +from . import utils BLOB_HEX = 'af431f20fc541ed6d5afede3e2dc7160f6f01f16' BLOB_RAW = binascii.unhexlify(BLOB_HEX.encode('ascii')) diff --git a/test/test_oid.py b/test/test_oid.py index c6cbf3e8..41170226 100644 --- a/test/test_oid.py +++ b/test/test_oid.py @@ -28,9 +28,9 @@ # Standard Library from binascii import unhexlify -from pygit2 import Oid import pytest +from pygit2 import Oid HEX = '15b648aec6ed045b5ca6f57f8b7831a8b4757298' RAW = unhexlify(HEX.encode('ascii')) diff --git a/test/test_packbuilder.py b/test/test_packbuilder.py index 6d4ed0d9..cea99b39 100644 --- a/test/test_packbuilder.py +++ b/test/test_packbuilder.py @@ -29,6 +29,7 @@ import pygit2 from pygit2 import PackBuilder + from . import utils diff --git a/test/test_patch.py b/test/test_patch.py index 5620f9b5..b6b7c630 100644 --- a/test/test_patch.py +++ b/test/test_patch.py @@ -23,9 +23,9 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -import pygit2 import pytest +import pygit2 BLOB_OLD_SHA = 'a520c24d85fbfc815d385957eed41406ca5a860b' BLOB_NEW_SHA = '3b18e512dba79e4c8300dd08aeb37f8e728b8dad' diff --git a/test/test_patch_encoding.py b/test/test_patch_encoding.py index 23f4aca1..30072cf4 100644 --- a/test/test_patch_encoding.py +++ b/test/test_patch_encoding.py @@ -25,7 +25,6 @@ import pygit2 - expected_diff = b"""diff --git a/iso-8859-1.txt b/iso-8859-1.txt index e84e339..201e0c9 100644 --- a/iso-8859-1.txt diff --git a/test/test_refdb_backend.py b/test/test_refdb_backend.py index a7f10cf5..cec430c2 100644 --- a/test/test_refdb_backend.py +++ b/test/test_refdb_backend.py @@ -27,9 +27,10 @@ from pathlib import Path -import pygit2 import pytest +import pygit2 + # Note: the refdb abstraction from libgit2 is meant to provide information # which libgit2 transforms into something more useful, and in general YMMV by diff --git a/test/test_refs.py b/test/test_refs.py index a2fe0ef5..4ffdca8a 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -29,11 +29,18 @@ import pytest -from pygit2 import Commit, Signature, Tree, reference_is_valid_name, Repository -from pygit2 import AlreadyExistsError, GitError, InvalidSpecError +from pygit2 import ( + AlreadyExistsError, + Commit, + GitError, + InvalidSpecError, + Repository, + Signature, + Tree, + reference_is_valid_name, +) from pygit2.enums import ReferenceType - LAST_COMMIT = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' diff --git a/test/test_remote.py b/test/test_remote.py index e3cc2146..e0e4318c 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -24,16 +24,16 @@ # Boston, MA 02110-1301, USA. import sys -from pathlib import Path from collections.abc import Generator +from pathlib import Path import pytest import pygit2 -from pygit2 import Repository, Remote +from pygit2 import Remote, Repository from pygit2.remotes import TransferProgress -from . import utils +from . import utils REMOTE_NAME = 'origin' REMOTE_URL = 'https://github.com/libgit2/pygit2.git' diff --git a/test/test_remote_utf8.py b/test/test_remote_utf8.py index cf58a8d5..1307e897 100644 --- a/test/test_remote_utf8.py +++ b/test/test_remote_utf8.py @@ -23,8 +23,10 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -import pygit2 import pytest + +import pygit2 + from . import utils diff --git a/test/test_repository.py b/test/test_repository.py index d48aa7ac..3dd47bbc 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -23,28 +23,34 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -from pathlib import Path import shutil import tempfile +from pathlib import Path import pytest # pygit2 import pygit2 -from pygit2 import init_repository, clone_repository, discover_repository, IndexEntry -from pygit2 import Oid +from pygit2 import ( + IndexEntry, + Oid, + clone_repository, + discover_repository, + init_repository, +) from pygit2.enums import ( CheckoutNotify, CheckoutStrategy, + FileMode, FileStatus, ObjectType, RepositoryOpenFlag, RepositoryState, ResetMode, StashApplyProgress, - FileMode, ) from pygit2.index import MergeFileResult + from . import utils diff --git a/test/test_repository_bare.py b/test/test_repository_bare.py index 0274a401..83392155 100644 --- a/test/test_repository_bare.py +++ b/test/test_repository_bare.py @@ -33,8 +33,8 @@ import pygit2 from pygit2.enums import FileMode, ObjectType -from . import utils +from . import utils HEAD_SHA = '784855caf26449a1914d2cf62d12b9374d76ae78' PARENT_SHA = 'f5e5aa4e36ab0fe62ee1ccc6eb8f79b866863b87' # HEAD^ diff --git a/test/test_repository_custom.py b/test/test_repository_custom.py index 5c365e09..bc4f8b9d 100644 --- a/test/test_repository_custom.py +++ b/test/test_repository_custom.py @@ -24,6 +24,7 @@ # Boston, MA 02110-1301, USA. from pathlib import Path + import pytest import pygit2 diff --git a/test/test_revparse.py b/test/test_revparse.py index 10effc49..a83f77db 100644 --- a/test/test_revparse.py +++ b/test/test_revparse.py @@ -25,9 +25,10 @@ """Tests for revision parsing.""" +from pytest import raises + from pygit2 import InvalidSpecError from pygit2.enums import RevSpecFlag -from pytest import raises HEAD_SHA = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' PARENT_SHA = '5ebeeebb320790caf276b9fc8b24546d63316533' # HEAD^ diff --git a/test/test_revwalk.py b/test/test_revwalk.py index 483984c0..fa97456d 100644 --- a/test/test_revwalk.py +++ b/test/test_revwalk.py @@ -27,7 +27,6 @@ from pygit2.enums import SortMode - # In the order given by git log log = [ '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98', diff --git a/test/test_submodule.py b/test/test_submodule.py index dcfc5829..52c55b2b 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -28,13 +28,14 @@ from pathlib import Path from typing import Generator +import pytest + import pygit2 from pygit2 import Repository, Submodule -import pytest +from pygit2.enums import SubmoduleIgnore as SI +from pygit2.enums import SubmoduleStatus as SS from . import utils -from pygit2.enums import SubmoduleIgnore as SI, SubmoduleStatus as SS - SUBM_NAME = 'TestGitRepository' SUBM_PATH = 'TestGitRepository' diff --git a/test/test_tag.py b/test/test_tag.py index e0e73322..e641dded 100644 --- a/test/test_tag.py +++ b/test/test_tag.py @@ -30,7 +30,6 @@ import pygit2 from pygit2.enums import ObjectType - TAG_SHA = '3d2962987c695a29f1f80b6c3aa4ec046ef44369' diff --git a/test/test_tree.py b/test/test_tree.py index c5000083..85a22591 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -24,6 +24,7 @@ # Boston, MA 02110-1301, USA. import operator + import pytest import pygit2 @@ -31,7 +32,6 @@ from . import utils - TREE_SHA = '967fce8df97cc71722d3c2a5930ef3e6f1d27b12' SUBTREE_SHA = '614fd9a3094bf618ea938fffc00e7d1a54f89ad0' diff --git a/test/utils.py b/test/utils.py index 645a95bb..ba98f5ce 100644 --- a/test/utils.py +++ b/test/utils.py @@ -25,14 +25,14 @@ # Standard library import hashlib -from pathlib import Path import shutil import socket import stat import sys import zipfile -from typing import Optional +from pathlib import Path from types import TracebackType +from typing import Optional # Requirements import pytest @@ -40,7 +40,6 @@ # Pygit2 import pygit2 - requires_future_libgit2 = pytest.mark.xfail( pygit2.LIBGIT2_VER < (2, 0, 0), reason='This test may work with a future version of libgit2', From 41a76dd0bad71da57ade31b651de9909ffe17faa Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Sat, 26 Jul 2025 13:44:26 +0200 Subject: [PATCH 9/9] make sure imports stay sorted also fix ruff related stuff --- .github/workflows/lint.yml | 5 ++++- pygit2/_pygit2.pyi | 2 +- pygit2/ffi.py | 2 +- pyproject.toml | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5c4f145f..57db3c45 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,8 +22,11 @@ jobs: - name: Install ruff run: pip install ruff - - name: Check code style with ruff + - name: Format code with ruff run: ruff format --diff + - name: Check code style with ruff + run: ruff check + - name: Check typing with mypy run: LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh mypy diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 29c99084..88a10d7d 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -50,7 +50,7 @@ from .enums import ( ) from .remotes import Remote from .repository import BaseRepository -from .submodules import Submodule, SubmoduleCollection +from .submodules import SubmoduleCollection GIT_OBJ_BLOB = Literal[3] GIT_OBJ_COMMIT = Literal[1] diff --git a/pygit2/ffi.py b/pygit2/ffi.py index 6fe791a5..41ebec4c 100644 --- a/pygit2/ffi.py +++ b/pygit2/ffi.py @@ -25,4 +25,4 @@ # Import from pygit2 from ._libgit2 import ffi # type: ignore # noqa: F401 -from ._libgit2 import lib as C +from ._libgit2 import lib as C # type: ignore # noqa: F401 diff --git a/pyproject.toml b/pyproject.toml index c625df47..219efcac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,9 @@ repair-wheel-command = "DYLD_LIBRARY_PATH=/Users/runner/work/pygit2/pygit2/ci/li extend-exclude = [ ".cache", ".coverage", "build", "site-packages", "venv*"] target-version = "py310" # oldest supported Python version +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I"] + [tool.ruff.format] quote-style = "single"