From f8d9a05bc8e2829ddec72cc0a246e37bffe94ac3 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 31 Jul 2025 17:06:12 +0200 Subject: [PATCH 1/6] improve types according to test/test_refdb_backend.py also renamed testrepo to self in ProxyRefdbBackend class --- pygit2/_pygit2.pyi | 10 ++++-- test/test_refdb_backend.py | 73 ++++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 1af40174..28aebbb3 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -643,7 +643,9 @@ class Refdb: class RefdbBackend: def __init__(self, *args, **kwargs) -> None: ... def compress(self) -> None: ... - def delete(self, ref_name: str, old_id: _OidArg, old_target: str) -> None: ... + def delete( + self, ref_name: str, old_id: _OidArg, old_target: str | None + ) -> None: ... def ensure_log(self, ref_name: str) -> bool: ... def exists(self, refname: str) -> bool: ... def has_log(self, ref_name: str) -> bool: ... @@ -657,9 +659,10 @@ class RefdbBackend: force: bool, who: Signature, message: str, - old: _OidArg, - old_target: str, + old: None | _OidArg, + old_target: None | str, ) -> None: ... + def __iter__(self) -> Iterator[Reference]: ... class RefdbFsBackend(RefdbBackend): def __init__(self, *args, **kwargs) -> None: ... @@ -738,6 +741,7 @@ class Branches: class Repository: _pointer: GitRepositoryC _repo: GitRepositoryC + backend: RefdbBackend default_signature: Signature head: Reference head_is_detached: bool diff --git a/test/test_refdb_backend.py b/test/test_refdb_backend.py index cec430c2..903b9388 100644 --- a/test/test_refdb_backend.py +++ b/test/test_refdb_backend.py @@ -26,10 +26,12 @@ """Tests for Refdb objects.""" from pathlib import Path +from typing import Generator, Iterator import pytest import pygit2 +from pygit2 import Commit, Oid, Reference, Repository, Signature # Note: the refdb abstraction from libgit2 is meant to provide information @@ -38,77 +40,88 @@ # incomplete, to avoid hitting the semi-valid states that refdbs produce by # design. class ProxyRefdbBackend(pygit2.RefdbBackend): - def __init__(testrepo, source): - testrepo.source = source + def __init__(self, source: pygit2.RefdbBackend) -> None: + self.source = source - def exists(testrepo, ref): - return testrepo.source.exists(ref) + def exists(self, ref: str) -> bool: + return self.source.exists(ref) - def lookup(testrepo, ref): - return testrepo.source.lookup(ref) + def lookup(self, ref: str) -> Reference: + return self.source.lookup(ref) - def write(testrepo, ref, force, who, message, old, old_target): - return testrepo.source.write(ref, force, who, message, old, old_target) + def write( + self, + ref: Reference, + force: bool, + who: Signature, + message: str, + old: None | str | Oid, + old_target: None | str, + ) -> None: + return self.source.write(ref, force, who, message, old, old_target) - def rename(testrepo, old_name, new_name, force, who, message): - return testrepo.source.rename(old_name, new_name, force, who, message) + def rename( + self, old_name: str, new_name: str, force: bool, who: Signature, message: str + ) -> Reference: + return self.source.rename(old_name, new_name, force, who, message) - def delete(testrepo, ref_name, old_id, old_target): - return testrepo.source.delete(ref_name, old_id, old_target) + def delete(self, ref_name: str, old_id: Oid | str, old_target: str | None) -> None: + return self.source.delete(ref_name, old_id, old_target) - def compress(testrepo): - return testrepo.source.compress() + def compress(self) -> None: + return self.source.compress() - def has_log(testrepo, ref_name): - return testrepo.source.has_log(ref_name) + def has_log(self, ref_name: str) -> bool: + return self.source.has_log(ref_name) - def ensure_log(testrepo, ref_name): - return testrepo.source.ensure_log(ref_name) + def ensure_log(self, ref_name: str) -> bool: + return self.source.ensure_log(ref_name) - def __iter__(testrepo): - return iter(testrepo.source) + def __iter__(self) -> Iterator[Reference]: + return iter(self.source) @pytest.fixture -def repo(testrepo): +def repo(testrepo: Repository) -> Generator[Repository, None, None]: testrepo.backend = ProxyRefdbBackend(pygit2.RefdbFsBackend(testrepo)) yield testrepo -def test_exists(repo): +def test_exists(repo: Repository) -> None: assert not repo.backend.exists('refs/heads/does-not-exist') assert repo.backend.exists('refs/heads/master') -def test_lookup(repo): +def test_lookup(repo: Repository) -> None: assert repo.backend.lookup('refs/heads/does-not-exist') is None assert repo.backend.lookup('refs/heads/master').name == 'refs/heads/master' -def test_write(repo): +def test_write(repo: Repository) -> None: master = repo.backend.lookup('refs/heads/master') - commit = repo.get(master.target) + commit = repo[master.target] ref = pygit2.Reference('refs/heads/test-write', master.target, None) repo.backend.write(ref, False, commit.author, 'Create test-write', None, None) assert repo.backend.lookup('refs/heads/test-write').target == master.target -def test_rename(repo): +def test_rename(repo: Repository) -> None: old_ref = repo.backend.lookup('refs/heads/i18n') target = repo.get(old_ref.target) + assert isinstance(target, Commit) repo.backend.rename( 'refs/heads/i18n', 'refs/heads/intl', False, target.committer, target.message ) assert repo.backend.lookup('refs/heads/intl').target == target.id -def test_delete(repo): +def test_delete(repo: Repository) -> None: old = repo.backend.lookup('refs/heads/i18n') repo.backend.delete('refs/heads/i18n', old.target, None) assert not repo.backend.lookup('refs/heads/i18n') -def test_compress(repo): +def test_compress(repo: Repository) -> None: repo = repo packed_refs_file = Path(repo.path) / 'packed-refs' assert not packed_refs_file.exists() @@ -116,12 +129,12 @@ def test_compress(repo): assert packed_refs_file.exists() -def test_has_log(repo): +def test_has_log(repo: Repository) -> None: assert repo.backend.has_log('refs/heads/master') assert not repo.backend.has_log('refs/heads/does-not-exist') -def test_ensure_log(repo): +def test_ensure_log(repo: Repository) -> None: assert not repo.backend.has_log('refs/heads/new-log') repo.backend.ensure_log('refs/heads/new-log') assert repo.backend.has_log('refs/heads/new-log') From edcccfc9c0ec32748e11fbf93f5e12009ba680ef Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 31 Jul 2025 17:26:45 +0200 Subject: [PATCH 2/6] improve types according to test/test_refs.py --- pygit2/_pygit2.pyi | 8 ++++- test/test_refs.py | 82 +++++++++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 28aebbb3..43decedb 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -845,7 +845,11 @@ class Repository: force: bool = False, ) -> Oid: ... def create_reference( - self, name: str, target: _OidArg, force: bool = False + self, + name: str, + target: _OidArg, + force: bool = False, + message: str | None = None, ) -> Reference: ... def create_reference_direct( self, name: str, target: _OidArg, force: bool, message: Optional[str] = None @@ -893,6 +897,7 @@ class Repository: def listall_branches(self, flag: BranchType = BranchType.LOCAL) -> list[str]: ... def listall_mergeheads(self) -> list[Oid]: ... def listall_references(self) -> list[str]: ... + def listall_reference_objects(self) -> list[Reference]: ... def listall_stashes(self) -> list[Stash]: ... def listall_submodules(self) -> list[str]: ... def lookup_branch( @@ -962,6 +967,7 @@ class Repository: references_return_type: ReferenceFilter = ReferenceFilter.ALL, ) -> Reference: ... def reset(self, oid: _OidArg, reset_type: ResetMode) -> None: ... + def resolve_refish(self, refresh: str) -> tuple[Commit, Reference]: ... def revparse(self, revspec: str) -> RevSpec: ... def revparse_ext(self, revision: str) -> tuple[Object, Reference]: ... def revparse_single(self, revision: str) -> Object: ... diff --git a/test/test_refs.py b/test/test_refs.py index 4ffdca8a..e1eced7a 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -34,17 +34,18 @@ Commit, GitError, InvalidSpecError, + Oid, Repository, Signature, Tree, reference_is_valid_name, ) -from pygit2.enums import ReferenceType +from pygit2.enums import ReferenceFilter, ReferenceType LAST_COMMIT = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' -def test_refs_list_objects(testrepo): +def test_refs_list_objects(testrepo: Repository) -> None: refs = [(ref.name, ref.target) for ref in testrepo.references.objects] assert sorted(refs) == [ ('refs/heads/i18n', '5470a671a80ac3789f1a6a8cefbcf43ce7af0563'), @@ -292,7 +293,7 @@ def test_refs_compress(testrepo: Repository) -> None: # -def test_list_all_reference_objects(testrepo): +def test_list_all_reference_objects(testrepo: Repository) -> None: repo = testrepo refs = [(ref.name, ref.target) for ref in repo.listall_reference_objects()] @@ -302,7 +303,7 @@ def test_list_all_reference_objects(testrepo): ] -def test_list_all_references(testrepo): +def test_list_all_references(testrepo: Repository) -> None: repo = testrepo # Without argument @@ -326,14 +327,14 @@ def test_list_all_references(testrepo): ] -def test_references_iterator_init(testrepo): +def test_references_iterator_init(testrepo: Repository) -> None: repo = testrepo iter = repo.references_iterator_init() assert iter.__class__.__name__ == 'RefsIterator' -def test_references_iterator_next(testrepo): +def test_references_iterator_next(testrepo: Repository) -> None: repo = testrepo repo.create_reference( 'refs/tags/version1', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' @@ -359,7 +360,9 @@ def test_references_iterator_next(testrepo): iter_branches = repo.references_iterator_init() all_branches = [] for _ in range(4): - curr_ref = repo.references_iterator_next(iter_branches, 1) + curr_ref = repo.references_iterator_next( + iter_branches, ReferenceFilter.BRANCHES + ) if curr_ref: all_branches.append((curr_ref.name, curr_ref.target)) @@ -371,7 +374,7 @@ def test_references_iterator_next(testrepo): iter_tags = repo.references_iterator_init() all_tags = [] for _ in range(4): - curr_ref = repo.references_iterator_next(iter_tags, 2) + curr_ref = repo.references_iterator_next(iter_tags, ReferenceFilter.TAGS) if curr_ref: all_tags.append((curr_ref.name, curr_ref.target)) @@ -381,7 +384,7 @@ def test_references_iterator_next(testrepo): ] -def test_references_iterator_next_python(testrepo): +def test_references_iterator_next_python(testrepo: Repository) -> None: repo = testrepo repo.create_reference( 'refs/tags/version1', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' @@ -398,41 +401,43 @@ def test_references_iterator_next_python(testrepo): ('refs/tags/version2', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98'), ] - branches = [(x.name, x.target) for x in repo.references.iterator(1)] + branches = [ + (x.name, x.target) for x in repo.references.iterator(ReferenceFilter.BRANCHES) + ] assert sorted(branches) == [ ('refs/heads/i18n', '5470a671a80ac3789f1a6a8cefbcf43ce7af0563'), ('refs/heads/master', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98'), ] - tags = [(x.name, x.target) for x in repo.references.iterator(2)] + tags = [(x.name, x.target) for x in repo.references.iterator(ReferenceFilter.TAGS)] assert sorted(tags) == [ ('refs/tags/version1', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98'), ('refs/tags/version2', '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98'), ] -def test_references_iterator_invalid_filter(testrepo): +def test_references_iterator_invalid_filter(testrepo: Repository) -> None: repo = testrepo iter_all = repo.references_iterator_init() all_refs = [] for _ in range(4): - curr_ref = repo.references_iterator_next(iter_all, 5) + curr_ref = repo.references_iterator_next(iter_all, 5) # type: ignore if curr_ref: all_refs.append((curr_ref.name, curr_ref.target)) assert all_refs == [] -def test_references_iterator_invalid_filter_python(testrepo): +def test_references_iterator_invalid_filter_python(testrepo: Repository) -> None: repo = testrepo refs = [] with pytest.raises(ValueError): - for ref in repo.references.iterator(5): + for ref in repo.references.iterator(5): # type: ignore refs.append((ref.name, ref.target)) -def test_lookup_reference(testrepo): +def test_lookup_reference(testrepo: Repository) -> None: repo = testrepo # Raise KeyError ? @@ -444,7 +449,7 @@ def test_lookup_reference(testrepo): assert reference.name == 'refs/heads/master' -def test_lookup_reference_dwim(testrepo): +def test_lookup_reference_dwim(testrepo: Repository) -> None: repo = testrepo # remote ref @@ -474,7 +479,7 @@ def test_lookup_reference_dwim(testrepo): assert reference.name == 'refs/tags/version1' -def test_resolve_refish(testrepo): +def test_resolve_refish(testrepo: Repository) -> None: repo = testrepo # remote ref @@ -516,37 +521,37 @@ def test_resolve_refish(testrepo): assert commit.id == '5ebeeebb320790caf276b9fc8b24546d63316533' -def test_reference_get_sha(testrepo): +def test_reference_get_sha(testrepo: Repository) -> None: reference = testrepo.lookup_reference('refs/heads/master') assert reference.target == LAST_COMMIT -def test_reference_set_sha(testrepo): +def test_reference_set_sha(testrepo: Repository) -> None: NEW_COMMIT = '5ebeeebb320790caf276b9fc8b24546d63316533' reference = testrepo.lookup_reference('refs/heads/master') reference.set_target(NEW_COMMIT) assert reference.target == NEW_COMMIT -def test_reference_set_sha_prefix(testrepo): +def test_reference_set_sha_prefix(testrepo: Repository) -> None: NEW_COMMIT = '5ebeeebb320790caf276b9fc8b24546d63316533' reference = testrepo.lookup_reference('refs/heads/master') reference.set_target(NEW_COMMIT[0:6]) assert reference.target == NEW_COMMIT -def test_reference_get_type(testrepo): +def test_reference_get_type(testrepo: Repository) -> None: reference = testrepo.lookup_reference('refs/heads/master') assert reference.type == ReferenceType.DIRECT -def test_get_target(testrepo): +def test_get_target(testrepo: Repository) -> None: reference = testrepo.lookup_reference('HEAD') assert reference.target == 'refs/heads/master' assert reference.raw_target == b'refs/heads/master' -def test_set_target(testrepo): +def test_set_target(testrepo: Repository) -> None: reference = testrepo.lookup_reference('HEAD') assert reference.target == 'refs/heads/master' assert reference.raw_target == b'refs/heads/master' @@ -555,14 +560,14 @@ def test_set_target(testrepo): assert reference.raw_target == b'refs/heads/i18n' -def test_get_shorthand(testrepo): +def test_get_shorthand(testrepo: Repository) -> None: reference = testrepo.lookup_reference('refs/heads/master') assert reference.shorthand == 'master' reference = testrepo.create_reference('refs/remotes/origin/master', LAST_COMMIT) assert reference.shorthand == 'origin/master' -def test_set_target_with_message(testrepo): +def test_set_target_with_message(testrepo: Repository) -> None: reference = testrepo.lookup_reference('HEAD') assert reference.target == 'refs/heads/master' assert reference.raw_target == b'refs/heads/master' @@ -577,7 +582,7 @@ def test_set_target_with_message(testrepo): assert first.committer == sig -def test_delete(testrepo): +def test_delete(testrepo: Repository) -> None: repo = testrepo # We add a tag as a new reference that points to "origin/master" @@ -605,7 +610,7 @@ def test_delete(testrepo): reference.rename('refs/tags/version2') -def test_rename(testrepo): +def test_rename(testrepo: Repository) -> None: # We add a tag as a new reference that points to "origin/master" reference = testrepo.create_reference('refs/tags/version1', LAST_COMMIT) assert reference.name == 'refs/tags/version1' @@ -613,7 +618,7 @@ def test_rename(testrepo): assert reference.name == 'refs/tags/version2' -# def test_reload(testrepo): +# def test_reload(testrepo: Repository) -> None: # name = 'refs/tags/version1' # repo = testrepo @@ -625,7 +630,7 @@ def test_rename(testrepo): # with pytest.raises(GitError): getattr(ref2, 'name') -def test_reference_resolve(testrepo): +def test_reference_resolve(testrepo: Repository) -> None: reference = testrepo.lookup_reference('HEAD') assert reference.type == ReferenceType.SYMBOLIC reference = reference.resolve() @@ -633,13 +638,13 @@ def test_reference_resolve(testrepo): assert reference.target == LAST_COMMIT -def test_reference_resolve_identity(testrepo): +def test_reference_resolve_identity(testrepo: Repository) -> None: head = testrepo.lookup_reference('HEAD') ref = head.resolve() assert ref.resolve() is ref -def test_create_reference(testrepo): +def test_create_reference(testrepo: Repository) -> None: # We add a tag as a new reference that points to "origin/master" reference = testrepo.create_reference('refs/tags/version1', LAST_COMMIT) assert 'refs/tags/version1' in testrepo.listall_references() @@ -660,7 +665,7 @@ def test_create_reference(testrepo): assert reference.target == LAST_COMMIT -def test_create_reference_with_message(testrepo): +def test_create_reference_with_message(testrepo: Repository) -> None: sig = Signature('foo', 'bar') testrepo.set_ident('foo', 'bar') msg = 'Hello log' @@ -672,7 +677,7 @@ def test_create_reference_with_message(testrepo): assert first.committer == sig -def test_create_symbolic_reference(testrepo): +def test_create_symbolic_reference(testrepo: Repository) -> None: repo = testrepo # We add a tag as a new symbolic reference that always points to # "refs/heads/master" @@ -693,7 +698,7 @@ def test_create_symbolic_reference(testrepo): assert reference.raw_target == b'refs/heads/master' -def test_create_symbolic_reference_with_message(testrepo): +def test_create_symbolic_reference_with_message(testrepo: Repository) -> None: sig = Signature('foo', 'bar') testrepo.set_ident('foo', 'bar') msg = 'Hello log' @@ -705,7 +710,7 @@ def test_create_symbolic_reference_with_message(testrepo): assert first.committer == sig -def test_create_invalid_reference(testrepo): +def test_create_invalid_reference(testrepo: Repository) -> None: repo = testrepo # try to create a reference with an invalid name @@ -714,14 +719,15 @@ def test_create_invalid_reference(testrepo): assert isinstance(error.value, ValueError) -# def test_packall_references(testrepo): +# def test_packall_references(testrepo: Repository) -> None: # testrepo.packall_references() -def test_peel(testrepo): +def test_peel(testrepo: Repository) -> None: repo = testrepo ref = repo.lookup_reference('refs/heads/master') assert repo[ref.target].id == ref.peel().id + assert isinstance(ref.raw_target, Oid) assert repo[ref.raw_target].id == ref.peel().id commit = ref.peel(Commit) From fb0bfd76187392dc323974981e7fd10687766247 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 31 Jul 2025 20:19:54 +0200 Subject: [PATCH 3/6] improve types according to test/test_remote.py --- pygit2/_libgit2/ffi.pyi | 19 +++++++++++++++++ pygit2/remotes.py | 46 ++++++++++++++++++++++++++++++----------- test/test_remote.py | 8 ++++--- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index 4bf2637b..4cb37caa 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -35,6 +35,9 @@ NULL: NULL_TYPE = ... char = NewType('char', object) char_pointer = NewType('char_pointer', object) +class size_t: + def __getitem__(self, item: Literal[0]) -> int: ... + class _Pointer(Generic[T]): def __setitem__(self, item: Literal[0], a: T) -> None: ... @overload @@ -42,6 +45,9 @@ class _Pointer(Generic[T]): @overload def __getitem__(self, item: slice[None, None, None]) -> bytes: ... +class _MultiPointer(Generic[T]): + def __getitem__(self, item: int) -> T: ... + class ArrayC(Generic[T]): # incomplete! # def _len(self, ?) -> ?: ... @@ -83,6 +89,13 @@ class GitSubmoduleC: class GitSubmoduleUpdateOptionsC: fetch_opts: GitFetchOptionsC +class GitRemoteHeadC: + local: int + oid: GitOidC + loid: GitOidC + name: char_pointer + symref_target: char_pointer + class UnsignedIntC: def __getitem__(self, item: Literal[0]) -> int: ... @@ -284,6 +297,12 @@ def new(a: Literal['git_signature *']) -> GitSignatureC: ... @overload def new(a: Literal['git_signature **']) -> _Pointer[GitSignatureC]: ... @overload +def new( + a: Literal['git_remote_head ***'], +) -> _Pointer[_MultiPointer[GitRemoteHeadC]]: ... +@overload +def new(a: Literal['size_t *']) -> size_t: ... +@overload def new(a: Literal['git_stash_save_options *']) -> GitStashSaveOptionsC: ... @overload def new(a: Literal['git_strarray *']) -> GitStrrayC: ... diff --git a/pygit2/remotes.py b/pygit2/remotes.py index 195d4822..1a82d1ce 100644 --- a/pygit2/remotes.py +++ b/pygit2/remotes.py @@ -25,11 +25,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any - -from . import utils +from typing import TYPE_CHECKING, Any, TypedDict # Import from pygit2 +from pygit2 import RemoteCallbacks + +from . import utils from ._pygit2 import Oid from .callbacks import ( git_fetch_options, @@ -48,6 +49,14 @@ from .repository import BaseRepository +class LsRemotesDict(TypedDict): + local: bool + loid: None | Oid + name: str | None + symref_target: str | None + oid: Oid + + class TransferProgress: """Progress downloading and indexing data during a fetch.""" @@ -180,7 +189,9 @@ def fetch( return TransferProgress(C.git_remote_stats(self._remote)) - def ls_remotes(self, callbacks=None, proxy=None): + def ls_remotes( + self, callbacks: RemoteCallbacks | None = None, proxy: str | None | bool = None + ) -> list[LsRemotesDict]: """ Return a list of dicts that maps to `git_remote_head` from a `ls_remotes` call. @@ -209,13 +220,15 @@ def ls_remotes(self, callbacks=None, proxy=None): else: loid = None - remote = { - 'local': local, - 'loid': loid, - 'name': maybe_string(ref.name), - 'symref_target': maybe_string(ref.symref_target), - 'oid': Oid(raw=bytes(ffi.buffer(ref.oid.id)[:])), - } + remote = LsRemotesDict( + { + 'local': local, + 'loid': loid, + 'name': maybe_string(ref.name), + 'symref_target': maybe_string(ref.symref_target), + 'oid': Oid(raw=bytes(ffi.buffer(ref.oid.id)[:])), + } + ) results.append(remote) @@ -256,7 +269,14 @@ def push_refspecs(self): check_error(err) return strarray_to_strings(specs) - def push(self, specs, callbacks=None, proxy=None, push_options=None, threads=1): + def push( + self, + specs: list[str], + callbacks: RemoteCallbacks | None = None, + proxy: None | bool | str = None, + push_options: None | list[str] = None, + threads: int = 1, + ) -> None: """ Push the given refspec to the remote. Raises ``GitError`` on protocol error or unpack failure. @@ -272,6 +292,8 @@ def push(self, specs, callbacks=None, proxy=None, push_options=None, threads=1): specs : [str] Push refspecs to use. + callbacks : + proxy : None or True or str Proxy configuration. Can be one of: diff --git a/test/test_remote.py b/test/test_remote.py index e0e4318c..67a6a34a 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -282,11 +282,13 @@ def test_update_tips(emptyrepo: Repository) -> None: ] class MyCallbacks(pygit2.RemoteCallbacks): - def __init__(self, tips): + tips: list[tuple[str, pygit2.Oid, pygit2.Oid]] + + def __init__(self, tips: list[tuple[str, pygit2.Oid, pygit2.Oid]]) -> None: self.tips = tips self.i = 0 - def update_tips(self, name, old, new): + def update_tips(self, name: str, old: pygit2.Oid, new: pygit2.Oid) -> None: assert self.tips[self.i] == (name, old, new) self.i += 1 @@ -342,7 +344,7 @@ def clone(tmp_path: Path) -> Generator[Repository, None, None]: @pytest.fixture -def remote(origin, clone): +def remote(origin: Repository, clone: Repository) -> Generator[Remote, None, None]: yield clone.remotes.create('origin', origin.path) From cf67fab2ab6bb8129b000e6df864d824fed45764 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 31 Jul 2025 20:25:18 +0200 Subject: [PATCH 4/6] improve types according to test/test_remote_prune.py --- pygit2/remotes.py | 2 +- test/test_remote_prune.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pygit2/remotes.py b/pygit2/remotes.py index 1a82d1ce..93f18479 100644 --- a/pygit2/remotes.py +++ b/pygit2/remotes.py @@ -234,7 +234,7 @@ def ls_remotes( return results - def prune(self, callbacks=None): + def prune(self, callbacks: RemoteCallbacks | None = None) -> None: """Perform a prune against this remote.""" with git_remote_callbacks(callbacks) as payload: err = C.git_remote_prune(self._remote, payload.remote_callbacks) diff --git a/test/test_remote_prune.py b/test/test_remote_prune.py index 927d812c..ea0b8489 100644 --- a/test/test_remote_prune.py +++ b/test/test_remote_prune.py @@ -23,14 +23,20 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. +from pathlib import Path +from typing import Generator + import pytest import pygit2 +from pygit2 import Oid, Repository from pygit2.enums import FetchPrune @pytest.fixture -def clonerepo(testrepo, tmp_path): +def clonerepo( + testrepo: Repository, tmp_path: Path +) -> Generator[Repository, None, None]: cloned_repo_path = tmp_path / 'test_remote_prune' pygit2.clone_repository(testrepo.workdir, cloned_repo_path) @@ -39,26 +45,26 @@ def clonerepo(testrepo, tmp_path): yield clonerepo -def test_fetch_remote_default(clonerepo): +def test_fetch_remote_default(clonerepo: Repository) -> None: clonerepo.remotes[0].fetch() assert 'origin/i18n' in clonerepo.branches -def test_fetch_remote_prune(clonerepo): +def test_fetch_remote_prune(clonerepo: Repository) -> None: clonerepo.remotes[0].fetch(prune=FetchPrune.PRUNE) assert 'origin/i18n' not in clonerepo.branches -def test_fetch_no_prune(clonerepo): +def test_fetch_no_prune(clonerepo: Repository) -> None: clonerepo.remotes[0].fetch(prune=FetchPrune.NO_PRUNE) assert 'origin/i18n' in clonerepo.branches -def test_remote_prune(clonerepo): +def test_remote_prune(clonerepo: Repository) -> None: pruned = [] class MyCallbacks(pygit2.RemoteCallbacks): - def update_tips(self, name, old, new): + def update_tips(self, name: str, old: Oid, new: Oid) -> None: pruned.append(name) callbacks = MyCallbacks() From 876b755ac3e004d0e6a9f25d47b43e4b5ed09022 Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 31 Jul 2025 20:26:47 +0200 Subject: [PATCH 5/6] add types to test/test_remote_utf8.py --- test/test_remote_utf8.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test_remote_utf8.py b/test/test_remote_utf8.py index 1307e897..4ed0bdd2 100644 --- a/test/test_remote_utf8.py +++ b/test/test_remote_utf8.py @@ -23,6 +23,9 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. +from pathlib import Path +from typing import Generator + import pytest import pygit2 @@ -31,11 +34,11 @@ @pytest.fixture -def repo(tmp_path): +def repo(tmp_path: Path) -> Generator[pygit2.Repository, None, None]: with utils.TemporaryRepository('utf8branchrepo.zip', tmp_path) as path: yield pygit2.Repository(path) -def test_fetch(repo): +def test_fetch(repo: pygit2.Repository) -> None: remote = repo.remotes.create('origin', repo.workdir) remote.fetch() From 4dd179d2cebe89661b84192ae36c070cf944e3dd Mon Sep 17 00:00:00 2001 From: Benedikt Seidl Date: Thu, 31 Jul 2025 22:35:57 +0200 Subject: [PATCH 6/6] improve types according to test/test_repository.py --- pygit2/_pygit2.pyi | 60 +++++++++++- pygit2/index.py | 4 +- test/test_index.py | 4 +- test/test_repository.py | 205 ++++++++++++++++++++++++---------------- 4 files changed, 183 insertions(+), 90 deletions(-) diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 43decedb..d7ca90b3 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -17,7 +17,7 @@ from typing import ( overload, ) -from . import Index +from . import Index, IndexEntry from ._libgit2.ffi import ( GitCommitC, GitMergeOptionsC, @@ -28,7 +28,7 @@ from ._libgit2.ffi import ( _Pointer, ) from .blame import Blame -from .callbacks import CheckoutCallbacks +from .callbacks import CheckoutCallbacks, StashApplyCallbacks from .config import Config from .enums import ( ApplyLocation, @@ -59,6 +59,7 @@ from .enums import ( SortMode, ) from .filter import Filter +from .index import MergeFileResult from .packbuilder import PackBuilder from .remotes import Remote from .repository import BaseRepository @@ -765,7 +766,9 @@ class Repository: def _from_c(cls, ptr: 'GitRepositoryC', owned: bool) -> 'Repository': ... def __iter__(self) -> Iterator[Oid]: ... def __getitem__(self, key: str | Oid) -> Object: ... - def add_worktree(self, name: str, path: str, ref: Reference = ...) -> Worktree: ... + def add_worktree( + self, name: str, path: str | Path, ref: Reference = ... + ) -> Worktree: ... def amend_commit( self, commit: Commit | Oid | str, @@ -797,13 +800,14 @@ class Repository: ) -> Blame: ... def checkout( self, - refname: Optional[_OidArg], + refname: _OidArg | None | Reference = None, *, strategy: CheckoutStrategy | None = None, - directory: str | None = None, + directory: str | Path | None = None, paths: list[str] | None = None, callbacks: CheckoutCallbacks | None = None, ) -> None: ... + def ahead_behind(self, local: _OidArg, upstream: _OidArg) -> tuple[int, int]: ... def cherrypick(self, id: _OidArg) -> None: ... def compress_references(self) -> None: ... @property @@ -893,6 +897,12 @@ class Repository: commit: _OidArg | None = None, ) -> bool | None | str: ... def git_object_lookup_prefix(self, oid: _OidArg) -> Object: ... + def hashfile( + self, + path: str, + object_type: ObjectType = ObjectType.BLOB, + as_path: str | None = None, + ) -> Oid: ... def list_worktrees(self) -> list[str]: ... def listall_branches(self, flag: BranchType = BranchType.LOCAL) -> list[str]: ... def listall_mergeheads(self) -> list[Oid]: ... @@ -930,6 +940,13 @@ class Repository: flags: MergeFlag = MergeFlag.FIND_RENAMES, file_flags: MergeFileFlag = MergeFileFlag.DEFAULT, ) -> Index: ... + def merge_file_from_index( + self, + ancestor: IndexEntry | None, + ours: IndexEntry | None, + theirs: IndexEntry | None, + use_deprecated: bool = True, + ) -> str | MergeFileResult | None: ... @staticmethod def _merge_options( favor: int | MergeFavor, flags: int | MergeFlag, file_flags: int | MergeFileFlag @@ -971,12 +988,45 @@ class Repository: def revparse(self, revspec: str) -> RevSpec: ... def revparse_ext(self, revision: str) -> tuple[Object, Reference]: ... def revparse_single(self, revision: str) -> Object: ... + def revert(self, commit: Commit) -> None: ... + def revert_commit( + self, revert_commit: Commit, our_commit: Commit, mainline: int = 0 + ) -> Index: ... def set_ident(self, name: str, email: str) -> None: ... def set_odb(self, odb: Odb) -> None: ... def set_refdb(self, refdb: Refdb) -> None: ... def status( self, untracked_files: str = 'all', ignored: bool = False ) -> dict[str, int]: ... + def stash( + self, + stasher: Signature, + message: str | None = None, + keep_index: bool = False, + include_untracked: bool = False, + include_ignored: bool = False, + keep_all: bool = False, + paths: list[str] | None = None, + ) -> None: ... + def stash_apply( + self, + index: int = 0, + reinstate_index: bool | None = None, + include_untracked: bool | None = None, + message: str | None = None, + strategy: CheckoutStrategy | None = None, + callbacks: StashApplyCallbacks | None = None, + ) -> None: ... + def stash_pop( + self, + index: int = 0, + reinstate_index: bool | None = None, + include_untracked: bool | None = None, + message: str | None = None, + strategy: CheckoutStrategy | None = None, + callbacks: StashApplyCallbacks | None = None, + ) -> None: ... + def stash_drop(self, index: int = 0) -> None: ... def status_file(self, path: str) -> int: ... def state(self) -> RepositoryState: ... def state_cleanup(self) -> None: ... diff --git a/pygit2/index.py b/pygit2/index.py index 1b52b797..3c9f9876 100644 --- a/pygit2/index.py +++ b/pygit2/index.py @@ -83,7 +83,7 @@ def __contains__(self, path) -> bool: check_error(err) return True - def __getitem__(self, key): + def __getitem__(self, key: str | int | PathLike[str]) -> 'IndexEntry': centry = ffi.NULL if isinstance(key, str) or hasattr(key, '__fspath__'): centry = C.git_index_get_bypath(self._index, to_bytes(key), 0) @@ -389,7 +389,7 @@ class MergeFileResult: automergeable: bool 'True if the output was automerged, false if the output contains conflict markers' - path: typing.Union[str, None] + path: typing.Union[str, None, PathLike[str]] 'The path that the resultant merge file should use, or None if a filename conflict would occur' mode: FileMode diff --git a/test/test_index.py b/test/test_index.py index 086863b7..6f3f3824 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -49,8 +49,8 @@ def test_read(testrepo: Repository) -> None: assert len(index) == 2 with pytest.raises(TypeError): - index[()] - utils.assertRaisesWithArg(ValueError, -4, lambda: index[-4]) + index[()] # type: ignore + utils.assertRaisesWithArg(ValueError, -4, lambda: index[-4]) # type: ignore utils.assertRaisesWithArg(KeyError, 'abc', lambda: index['abc']) sha = 'a520c24d85fbfc815d385957eed41406ca5a860b' diff --git a/test/test_repository.py b/test/test_repository.py index 3dd47bbc..5802ae4b 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -26,14 +26,19 @@ import shutil import tempfile from pathlib import Path +from typing import Optional import pytest # pygit2 import pygit2 from pygit2 import ( + Blob, + Commit, + DiffFile, IndexEntry, Oid, + Repository, clone_repository, discover_repository, init_repository, @@ -54,31 +59,31 @@ from . import utils -def test_is_empty(testrepo): +def test_is_empty(testrepo: Repository) -> None: assert not testrepo.is_empty -def test_is_bare(testrepo): +def test_is_bare(testrepo: Repository) -> None: assert not testrepo.is_bare -def test_get_path(testrepo_path): +def test_get_path(testrepo_path: tuple[Repository, Path]) -> None: testrepo, path = testrepo_path assert Path(testrepo.path).resolve() == (path / '.git').resolve() -def test_get_workdir(testrepo_path): +def test_get_workdir(testrepo_path: tuple[Repository, Path]) -> None: testrepo, path = testrepo_path assert Path(testrepo.workdir).resolve() == path.resolve() -def test_set_workdir(testrepo): +def test_set_workdir(testrepo: Repository) -> None: directory = tempfile.mkdtemp() testrepo.workdir = directory assert Path(testrepo.workdir).resolve() == Path(directory).resolve() -def test_checkout_ref(testrepo): +def test_checkout_ref(testrepo: Repository) -> None: ref_i18n = testrepo.lookup_reference('refs/heads/i18n') # checkout i18n with conflicts and default strategy should @@ -87,39 +92,48 @@ def test_checkout_ref(testrepo): testrepo.checkout(ref_i18n) # checkout i18n with GIT_CHECKOUT_FORCE - head = testrepo.head - head = testrepo[head.target] + head_object = testrepo.head + head = testrepo[head_object.target] assert 'new' not in head.tree testrepo.checkout(ref_i18n, strategy=CheckoutStrategy.FORCE) - head = testrepo.head - head = testrepo[head.target] + head_object = testrepo.head + head = testrepo[head_object.target] assert head.id == ref_i18n.target assert 'new' in head.tree assert 'bye.txt' not in testrepo.status() -def test_checkout_callbacks(testrepo): +def test_checkout_callbacks(testrepo: Repository) -> None: ref_i18n = testrepo.lookup_reference('refs/heads/i18n') class MyCheckoutCallbacks(pygit2.CheckoutCallbacks): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.conflicting_paths = set() - self.updated_paths = set() + self.conflicting_paths: set[str] = set() + self.updated_paths: set[str] = set() self.completed_steps = -1 self.total_steps = -1 def checkout_notify_flags(self) -> CheckoutNotify: return CheckoutNotify.CONFLICT | CheckoutNotify.UPDATED - def checkout_notify(self, why, path, baseline, target, workdir): + def checkout_notify( + self, + why: CheckoutNotify, + path: str, + baseline: Optional[DiffFile], + target: Optional[DiffFile], + workdir: Optional[DiffFile], + ) -> None: if why == CheckoutNotify.CONFLICT: self.conflicting_paths.add(path) elif why == CheckoutNotify.UPDATED: self.updated_paths.add(path) - def checkout_progress(self, path: str, completed_steps: int, total_steps: int): + def checkout_progress( + self, path: str, completed_steps: int, total_steps: int + ) -> None: self.completed_steps = completed_steps self.total_steps = total_steps @@ -132,8 +146,8 @@ def checkout_progress(self, path: str, completed_steps: int, total_steps: int): assert -1 == callbacks.completed_steps # shouldn't have done anything # checkout i18n with GIT_CHECKOUT_FORCE - head = testrepo.head - head = testrepo[head.target] + head_object = testrepo.head + head = testrepo[head_object.target] assert 'new' not in head.tree callbacks = MyCheckoutCallbacks() testrepo.checkout(ref_i18n, strategy=CheckoutStrategy.FORCE, callbacks=callbacks) @@ -144,29 +158,38 @@ def checkout_progress(self, path: str, completed_steps: int, total_steps: int): assert callbacks.completed_steps == callbacks.total_steps -def test_checkout_aborted_from_callbacks(testrepo): +def test_checkout_aborted_from_callbacks(testrepo: Repository) -> None: ref_i18n = testrepo.lookup_reference('refs/heads/i18n') - def read_bye_txt(): - return testrepo[testrepo.create_blob_fromworkdir('bye.txt')].data + def read_bye_txt() -> bytes: + blob = testrepo[testrepo.create_blob_fromworkdir('bye.txt')] + assert isinstance(blob, Blob) + return blob.data s = testrepo.status() assert s == {'bye.txt': FileStatus.WT_NEW} class MyCheckoutCallbacks(pygit2.CheckoutCallbacks): - def __init__(self): + def __init__(self) -> None: super().__init__() self.invoked_times = 0 - def checkout_notify(self, why, path, baseline, target, workdir): + def checkout_notify( + self, + why: CheckoutNotify, + path: str, + baseline: Optional[DiffFile], + target: Optional[DiffFile], + workdir: Optional[DiffFile], + ) -> None: self.invoked_times += 1 # skip one file so we're certain that NO files are affected, # even if aborting the checkout from the second file if self.invoked_times == 2: raise InterruptedError('Stop the checkout!') - head = testrepo.head - head = testrepo[head.target] + head_object = testrepo.head + head = testrepo[head_object.target] assert 'new' not in head.tree assert b'bye world\n' == read_bye_txt() callbacks = MyCheckoutCallbacks() @@ -182,7 +205,7 @@ def checkout_notify(self, why, path, baseline, target, workdir): assert b'bye world\n' == read_bye_txt() -def test_checkout_branch(testrepo): +def test_checkout_branch(testrepo: Repository) -> None: branch_i18n = testrepo.lookup_branch('i18n') # checkout i18n with conflicts and default strategy should @@ -191,19 +214,19 @@ def test_checkout_branch(testrepo): testrepo.checkout(branch_i18n) # checkout i18n with GIT_CHECKOUT_FORCE - head = testrepo.head - head = testrepo[head.target] + head_object = testrepo.head + head = testrepo[head_object.target] assert 'new' not in head.tree testrepo.checkout(branch_i18n, strategy=CheckoutStrategy.FORCE) - head = testrepo.head - head = testrepo[head.target] + head_object = testrepo.head + head = testrepo[head_object.target] assert head.id == branch_i18n.target assert 'new' in head.tree assert 'bye.txt' not in testrepo.status() -def test_checkout_index(testrepo): +def test_checkout_index(testrepo: Repository) -> None: # some changes to working dir with (Path(testrepo.workdir) / 'hello.txt').open('w') as f: f.write('new content') @@ -214,7 +237,7 @@ def test_checkout_index(testrepo): assert 'hello.txt' not in testrepo.status() -def test_checkout_head(testrepo): +def test_checkout_head(testrepo: Repository) -> None: # some changes to the index with (Path(testrepo.workdir) / 'bye.txt').open('w') as f: f.write('new content') @@ -230,7 +253,7 @@ def test_checkout_head(testrepo): assert 'bye.txt' not in testrepo.status() -def test_checkout_alternative_dir(testrepo): +def test_checkout_alternative_dir(testrepo: Repository) -> None: ref_i18n = testrepo.lookup_reference('refs/heads/i18n') extra_dir = Path(testrepo.workdir) / 'extra-dir' extra_dir.mkdir() @@ -239,7 +262,7 @@ def test_checkout_alternative_dir(testrepo): assert not len(list(extra_dir.iterdir())) == 0 -def test_checkout_paths(testrepo): +def test_checkout_paths(testrepo: Repository) -> None: ref_i18n = testrepo.lookup_reference('refs/heads/i18n') ref_master = testrepo.lookup_reference('refs/heads/master') testrepo.checkout(ref_master) @@ -248,7 +271,7 @@ def test_checkout_paths(testrepo): assert status['new'] == FileStatus.INDEX_NEW -def test_merge_base(testrepo): +def test_merge_base(testrepo: Repository) -> None: commit = testrepo.merge_base( '5ebeeebb320790caf276b9fc8b24546d63316533', '4ec4389a8068641da2d6578db0419484972284c8', @@ -264,7 +287,7 @@ def test_merge_base(testrepo): assert testrepo.merge_base(indep, commit) is None -def test_descendent_of(testrepo): +def test_descendent_of(testrepo: Repository) -> None: assert not testrepo.descendant_of( '5ebeeebb320790caf276b9fc8b24546d63316533', '4ec4389a8068641da2d6578db0419484972284c8', @@ -289,7 +312,7 @@ def test_descendent_of(testrepo): ) -def test_ahead_behind(testrepo): +def test_ahead_behind(testrepo: Repository) -> None: ahead, behind = testrepo.ahead_behind( '5ebeeebb320790caf276b9fc8b24546d63316533', '4ec4389a8068641da2d6578db0419484972284c8', @@ -305,7 +328,7 @@ def test_ahead_behind(testrepo): assert 1 == behind -def test_reset_hard(testrepo): +def test_reset_hard(testrepo: Repository) -> None: ref = '5ebeeebb320790caf276b9fc8b24546d63316533' with (Path(testrepo.workdir) / 'hello.txt').open() as f: lines = f.readlines() @@ -322,7 +345,7 @@ def test_reset_hard(testrepo): assert 'bonjour le monde\n' not in lines -def test_reset_soft(testrepo): +def test_reset_soft(testrepo: Repository) -> None: ref = '5ebeeebb320790caf276b9fc8b24546d63316533' with (Path(testrepo.workdir) / 'hello.txt').open() as f: lines = f.readlines() @@ -343,7 +366,7 @@ def test_reset_soft(testrepo): diff[0] -def test_reset_mixed(testrepo): +def test_reset_mixed(testrepo: Repository) -> None: ref = '5ebeeebb320790caf276b9fc8b24546d63316533' with (Path(testrepo.workdir) / 'hello.txt').open() as f: lines = f.readlines() @@ -362,11 +385,12 @@ def test_reset_mixed(testrepo): # mixed reset will set the index to match working copy diff = testrepo.diff(cached=True) + assert diff.patch is not None assert 'hola mundo\n' in diff.patch assert 'bonjour le monde\n' in diff.patch -def test_stash(testrepo): +def test_stash(testrepo: Repository) -> None: stash_hash = '6aab5192f88018cb98a7ede99c242f43add5a2fd' stash_message = 'custom stash message' sig = pygit2.Signature( @@ -403,7 +427,7 @@ def test_stash(testrepo): testrepo.stash_pop() -def test_stash_partial(testrepo): +def test_stash_partial(testrepo: Repository) -> None: stash_message = 'custom stash message' sig = pygit2.Signature( name='Stasher', email='stasher@example.com', time=1641000000, offset=0 @@ -441,7 +465,7 @@ def stash_pathspecs(paths): assert stash_pathspecs(['hello.txt', 'bye.txt']) -def test_stash_progress_callback(testrepo): +def test_stash_progress_callback(testrepo: Repository) -> None: sig = pygit2.Signature( name='Stasher', email='stasher@example.com', time=1641000000, offset=0 ) @@ -474,7 +498,7 @@ def stash_apply_progress(self, progress: StashApplyProgress): ] -def test_stash_aborted_from_callbacks(testrepo): +def test_stash_aborted_from_callbacks(testrepo: Repository) -> None: sig = pygit2.Signature( name='Stasher', email='stasher@example.com', time=1641000000, offset=0 ) @@ -513,7 +537,7 @@ def stash_apply_progress(self, progress: StashApplyProgress): assert repo_stashes[0].message == 'On master: custom stash message' -def test_stash_apply_checkout_options(testrepo): +def test_stash_apply_checkout_options(testrepo: Repository) -> None: sig = pygit2.Signature( name='Stasher', email='stasher@example.com', time=1641000000, offset=0 ) @@ -529,7 +553,14 @@ def test_stash_apply_checkout_options(testrepo): # define callbacks that raise an InterruptedError when checkout detects a conflict class MyStashApplyCallbacks(pygit2.StashApplyCallbacks): - def checkout_notify(self, why, path, baseline, target, workdir): + def checkout_notify( + self, + why: CheckoutNotify, + path: str, + baseline: Optional[DiffFile], + target: Optional[DiffFile], + workdir: Optional[DiffFile], + ) -> None: if why == CheckoutNotify.CONFLICT: raise InterruptedError('Applying the stash would create a conflict') @@ -556,9 +587,12 @@ def checkout_notify(self, why, path, baseline, target, workdir): assert f.read() == 'stashed content' -def test_revert_commit(testrepo): +def test_revert_commit(testrepo: Repository) -> None: master = testrepo.head.peel() + assert isinstance(master, Commit) commit_to_revert = testrepo['4ec4389a8068641da2d6578db0419484972284c8'] + assert isinstance(commit_to_revert, Commit) + parent = commit_to_revert.parents[0] commit_diff_stats = parent.tree.diff_to_tree(commit_to_revert.tree).stats @@ -570,9 +604,10 @@ def test_revert_commit(testrepo): assert revert_diff_stats.files_changed == commit_diff_stats.files_changed -def test_revert(testrepo): +def test_revert(testrepo: Repository) -> None: hello_txt = Path(testrepo.workdir) / 'hello.txt' commit_to_revert = testrepo['4ec4389a8068641da2d6578db0419484972284c8'] + assert isinstance(commit_to_revert, Commit) assert testrepo.state() == RepositoryState.NONE assert not testrepo.message @@ -590,7 +625,7 @@ def test_revert(testrepo): ) -def test_default_signature(testrepo): +def test_default_signature(testrepo: Repository) -> None: config = testrepo.config config['user.name'] = 'Random J Hacker' config['user.email'] = 'rjh@example.com' @@ -797,7 +832,7 @@ def test_clone_with_proxy(tmp_path): # # assert repo.remotes[0].fetchspec == "refs/heads/test" -def test_worktree(testrepo): +def test_worktree(testrepo: Repository) -> None: worktree_name = 'foo' worktree_dir = Path(tempfile.mkdtemp()) # Delete temp path so that it's not present when we attempt to add the @@ -839,7 +874,7 @@ def _check_worktree(worktree): assert testrepo.list_worktrees() == [] -def test_worktree_aspath(testrepo): +def test_worktree_aspath(testrepo: Repository) -> None: worktree_name = 'foo' worktree_dir = Path(tempfile.mkdtemp()) # Delete temp path so that it's not present when we attempt to add the @@ -849,7 +884,7 @@ def test_worktree_aspath(testrepo): assert testrepo.list_worktrees() == [worktree_name] -def test_worktree_custom_ref(testrepo): +def test_worktree_custom_ref(testrepo: Repository) -> None: worktree_name = 'foo' worktree_dir = Path(tempfile.mkdtemp()) branch_name = 'version1' @@ -917,7 +952,7 @@ def test_open_extended(tmp_path): assert not repo.workdir -def test_is_shallow(testrepo): +def test_is_shallow(testrepo: Repository) -> None: assert not testrepo.is_shallow # create a dummy shallow file @@ -927,7 +962,7 @@ def test_is_shallow(testrepo): assert testrepo.is_shallow -def test_repository_hashfile(testrepo): +def test_repository_hashfile(testrepo: Repository) -> None: original_hash = testrepo.index['hello.txt'].id # Test simple use @@ -937,8 +972,8 @@ def test_repository_hashfile(testrepo): # Test absolute path # For best results on Windows, pass a pure POSIX path. (See https://github.com/libgit2/libgit2/issues/6825) absolute_path = Path(testrepo.workdir, 'hello.txt') - absolute_path = absolute_path.as_posix() # Windows compatibility - h = testrepo.hashfile(str(absolute_path)) + absolute_path_str = absolute_path.as_posix() # Windows compatibility + h = testrepo.hashfile(str(absolute_path_str)) assert h == original_hash # Test missing path @@ -950,7 +985,7 @@ def test_repository_hashfile(testrepo): testrepo.hashfile('hello.txt', ObjectType.OFS_DELTA) -def test_repository_hashfile_filter(testrepo): +def test_repository_hashfile_filter(testrepo: Repository) -> None: original_hash = testrepo.index['hello.txt'].id with open(Path(testrepo.workdir, 'hello.txt'), 'rb') as f: @@ -977,8 +1012,8 @@ def test_repository_hashfile_filter(testrepo): # Treat absolute path with filters. # For best results on Windows, pass a pure POSIX path. (See https://github.com/libgit2/libgit2/issues/6825) absolute_path = Path(testrepo.workdir, 'hellocrlf.txt') - absolute_path = absolute_path.as_posix() # Windows compatibility - h = testrepo.hashfile(str(absolute_path)) + absolute_path_str = absolute_path.as_posix() # Windows compatibility + h = testrepo.hashfile(str(absolute_path_str)) assert h == original_hash # Bypass filters @@ -995,65 +1030,75 @@ def test_repository_hashfile_filter(testrepo): h = testrepo.hashfile('hello.txt') -def test_merge_file_from_index_deprecated(testrepo): +def test_merge_file_from_index_deprecated(testrepo: Repository) -> None: hello_txt = testrepo.index['hello.txt'] hello_txt_executable = IndexEntry( hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE ) hello_world = IndexEntry('hello_world.txt', hello_txt.id, hello_txt.mode) + def get_hello_txt_from_repo() -> str: + blob = testrepo.get(hello_txt.id) + assert isinstance(blob, Blob) + return blob.data.decode() + # no change res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # executable switch on ours res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # executable switch on theirs res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt_executable) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # executable switch on both res = testrepo.merge_file_from_index( hello_txt, hello_txt_executable, hello_txt_executable ) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # path switch on ours res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # path switch on theirs res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_world) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # path switch on both res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_world) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # path switch on ours, executable flag switch on theirs res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt_executable) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() # path switch on theirs, executable flag switch on ours res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_world) - assert res == testrepo.get(hello_txt.id).data.decode() + assert res == get_hello_txt_from_repo() -def test_merge_file_from_index_non_deprecated(testrepo): +def test_merge_file_from_index_non_deprecated(testrepo: Repository) -> None: hello_txt = testrepo.index['hello.txt'] hello_txt_executable = IndexEntry( hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE ) hello_world = IndexEntry('hello_world.txt', hello_txt.id, hello_txt.mode) + def get_hello_txt_from_repo() -> str: + blob = testrepo.get(hello_txt.id) + assert isinstance(blob, Blob) + return blob.data.decode() + # no change res = testrepo.merge_file_from_index( hello_txt, hello_txt, hello_txt, use_deprecated=False ) assert res == MergeFileResult( - True, hello_txt.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode() + True, hello_txt.path, hello_txt.mode, get_hello_txt_from_repo() ) # executable switch on ours @@ -1064,7 +1109,7 @@ def test_merge_file_from_index_non_deprecated(testrepo): True, hello_txt.path, hello_txt_executable.mode, - testrepo.get(hello_txt.id).data.decode(), + get_hello_txt_from_repo(), ) # executable switch on theirs @@ -1075,7 +1120,7 @@ def test_merge_file_from_index_non_deprecated(testrepo): True, hello_txt.path, hello_txt_executable.mode, - testrepo.get(hello_txt.id).data.decode(), + get_hello_txt_from_repo(), ) # executable switch on both @@ -1086,7 +1131,7 @@ def test_merge_file_from_index_non_deprecated(testrepo): True, hello_txt.path, hello_txt_executable.mode, - testrepo.get(hello_txt.id).data.decode(), + get_hello_txt_from_repo(), ) # path switch on ours @@ -1094,7 +1139,7 @@ def test_merge_file_from_index_non_deprecated(testrepo): hello_txt, hello_world, hello_txt, use_deprecated=False ) assert res == MergeFileResult( - True, hello_world.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode() + True, hello_world.path, hello_txt.mode, get_hello_txt_from_repo() ) # path switch on theirs @@ -1102,16 +1147,14 @@ def test_merge_file_from_index_non_deprecated(testrepo): hello_txt, hello_txt, hello_world, use_deprecated=False ) assert res == MergeFileResult( - True, hello_world.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode() + True, hello_world.path, hello_txt.mode, get_hello_txt_from_repo() ) # path switch on both res = testrepo.merge_file_from_index( hello_txt, hello_world, hello_world, use_deprecated=False ) - assert res == MergeFileResult( - True, None, hello_txt.mode, testrepo.get(hello_txt.id).data.decode() - ) + assert res == MergeFileResult(True, None, hello_txt.mode, get_hello_txt_from_repo()) # path switch on ours, executable flag switch on theirs res = testrepo.merge_file_from_index( @@ -1121,7 +1164,7 @@ def test_merge_file_from_index_non_deprecated(testrepo): True, hello_world.path, hello_txt_executable.mode, - testrepo.get(hello_txt.id).data.decode(), + get_hello_txt_from_repo(), ) # path switch on theirs, executable flag switch on ours @@ -1132,5 +1175,5 @@ def test_merge_file_from_index_non_deprecated(testrepo): True, hello_world.path, hello_txt_executable.mode, - testrepo.get(hello_txt.id).data.decode(), + get_hello_txt_from_repo(), )