Skip to content

Commit 0d7e6f9

Browse files
committed
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
1 parent 11f9a67 commit 0d7e6f9

File tree

5 files changed

+107
-36
lines changed

5 files changed

+107
-36
lines changed

pygit2/_pygit2.pyi

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from .enums import (
1010
ApplyLocation,
1111
BranchType,
1212
BlobFilter,
13+
BlameFlag,
1314
CheckoutStrategy,
1415
DeltaStatus,
1516
DiffFind,
@@ -28,10 +29,13 @@ from .enums import (
2829
)
2930
from collections.abc import Generator
3031

32+
from ._pygit2_c import GitSignatureC, _Pointer
33+
3134
from .repository import BaseRepository
3235
from .submodules import SubmoduleCollection, Submodule
3336
from .remotes import Remote
3437
from .callbacks import CheckoutCallbacks
38+
from .blame import Blame
3539

3640
GIT_OBJ_BLOB = Literal[3]
3741
GIT_OBJ_COMMIT = Literal[1]
@@ -724,6 +728,16 @@ class Repository:
724728
def apply(
725729
self, diff: Diff, location: ApplyLocation = ApplyLocation.WORKDIR
726730
) -> None: ...
731+
def blame(
732+
self,
733+
path: str,
734+
flags: BlameFlag = BlameFlag.NORMAL,
735+
min_match_characters: int | None = None,
736+
newest_commit: _OidArg | None = None,
737+
oldest_commit: _OidArg | None = None,
738+
min_line: int | None = None,
739+
max_line: int | None = None,
740+
) -> Blame: ...
727741
def checkout(
728742
self,
729743
refname: Optional[_OidArg],
@@ -864,7 +878,7 @@ class RevSpec:
864878

865879
class Signature:
866880
_encoding: str | None
867-
_pointer: bytes
881+
_pointer: _Pointer[GitSignatureC]
868882
email: str
869883
name: str
870884
offset: int

pygit2/_pygit2_c.pyi

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import NewType, Generic, Literal, TypeVar, overload
2+
3+
T = TypeVar('T')
4+
5+
char = NewType('char', object)
6+
char_pointer = NewType('char_pointer', object)
7+
8+
class _Pointer(Generic[T]):
9+
@overload
10+
def __getitem__(self, item: Literal[0]) -> T: ...
11+
@overload
12+
def __getitem__(self, item: slice[None, None, None]) -> bytes: ...
13+
14+
class GitTimeC:
15+
# incomplete
16+
time: int
17+
offset: int
18+
19+
class GitSignatureC:
20+
name: char_pointer
21+
email: char_pointer
22+
when: GitTimeC
23+
24+
class GitHunkC:
25+
# incomplete
26+
boundary: char
27+
final_start_line_number: int
28+
final_signature: GitSignatureC
29+
orig_signature: GitSignatureC
30+
orig_start_line_number: int
31+
orig_path: str
32+
lines_in_hunk: int
33+
34+
class GitBlameC:
35+
# incomplete
36+
pass

pygit2/blame.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@
2323
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
2424
# Boston, MA 02110-1301, USA.
2525

26+
from typing import Iterator, TYPE_CHECKING
27+
2628
# Import from pygit2
2729
from .ffi import ffi, C
2830
from .utils import GenericIterator
29-
from ._pygit2 import Signature, Oid
31+
from ._pygit2 import Signature, Oid, Repository
32+
33+
if TYPE_CHECKING:
34+
from ._pygit2_c import GitSignatureC, GitHunkC, GitBlameC
3035

3136

32-
def wrap_signature(csig):
37+
def wrap_signature(csig: 'GitSignatureC') -> None | Signature:
3338
if not csig:
3439
return None
3540

@@ -43,88 +48,94 @@ def wrap_signature(csig):
4348

4449

4550
class BlameHunk:
51+
_blame: 'Blame'
52+
_hunk: 'GitHunkC'
53+
4654
@classmethod
47-
def _from_c(cls, blame, ptr):
55+
def _from_c(cls, blame: 'Blame', ptr: 'GitHunkC') -> 'BlameHunk':
4856
hunk = cls.__new__(cls)
4957
hunk._blame = blame
5058
hunk._hunk = ptr
5159
return hunk
5260

5361
@property
54-
def lines_in_hunk(self):
62+
def lines_in_hunk(self) -> int:
5563
"""Number of lines"""
5664
return self._hunk.lines_in_hunk
5765

5866
@property
59-
def boundary(self):
67+
def boundary(self) -> bool:
6068
"""Tracked to a boundary commit"""
6169
# Casting directly to bool via cffi does not seem to work
6270
return int(ffi.cast('int', self._hunk.boundary)) != 0
6371

6472
@property
65-
def final_start_line_number(self):
73+
def final_start_line_number(self) -> int:
6674
"""Final start line number"""
6775
return self._hunk.final_start_line_number
6876

6977
@property
70-
def final_committer(self):
78+
def final_committer(self) -> None | Signature:
7179
"""Final committer"""
7280
return wrap_signature(self._hunk.final_signature)
7381

7482
@property
75-
def final_commit_id(self):
83+
def final_commit_id(self) -> Oid:
7684
return Oid(
7785
raw=bytes(ffi.buffer(ffi.addressof(self._hunk, 'final_commit_id'))[:])
7886
)
7987

8088
@property
81-
def orig_start_line_number(self):
89+
def orig_start_line_number(self) -> int:
8290
"""Origin start line number"""
8391
return self._hunk.orig_start_line_number
8492

8593
@property
86-
def orig_committer(self):
94+
def orig_committer(self) -> None | Signature:
8795
"""Original committer"""
8896
return wrap_signature(self._hunk.orig_signature)
8997

9098
@property
91-
def orig_commit_id(self):
99+
def orig_commit_id(self) -> Oid:
92100
return Oid(
93101
raw=bytes(ffi.buffer(ffi.addressof(self._hunk, 'orig_commit_id'))[:])
94102
)
95103

96104
@property
97-
def orig_path(self):
105+
def orig_path(self) -> None | str:
98106
"""Original path"""
99107
path = self._hunk.orig_path
100108
if not path:
101109
return None
102110

103-
return ffi.string(path).decode('utf-8')
111+
return ffi.string(path).decode('utf-8') # type: ignore[no-any-return]
104112

105113

106114
class Blame:
115+
_repo: Repository
116+
_blame: 'GitBlameC'
117+
107118
@classmethod
108-
def _from_c(cls, repo, ptr):
119+
def _from_c(cls, repo: Repository, ptr: 'GitBlameC') -> 'Blame':
109120
blame = cls.__new__(cls)
110121
blame._repo = repo
111122
blame._blame = ptr
112123
return blame
113124

114-
def __del__(self):
125+
def __del__(self) -> None:
115126
C.git_blame_free(self._blame)
116127

117-
def __len__(self):
118-
return C.git_blame_get_hunk_count(self._blame)
128+
def __len__(self) -> int:
129+
return C.git_blame_get_hunk_count(self._blame) # type: ignore[no-any-return]
119130

120-
def __getitem__(self, index):
131+
def __getitem__(self, index: int) -> BlameHunk:
121132
chunk = C.git_blame_get_hunk_byindex(self._blame, index)
122133
if not chunk:
123134
raise IndexError
124135

125136
return BlameHunk._from_c(self, chunk)
126137

127-
def for_line(self, line_no):
138+
def for_line(self, line_no: int) -> BlameHunk:
128139
"""
129140
Returns the <BlameHunk> object for a given line given its number in the
130141
current Blame.
@@ -143,5 +154,5 @@ def for_line(self, line_no):
143154

144155
return BlameHunk._from_c(self, chunk)
145156

146-
def __iter__(self):
157+
def __iter__(self) -> Iterator[BlameHunk]:
147158
return GenericIterator(self)

pygit2/utils.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
# Import from pygit2
3030
from .ffi import ffi, C
3131

32+
from typing import Protocol, Iterator, TypeVar, Generic
33+
3234

3335
def maybe_string(ptr):
3436
if not ptr:
@@ -153,22 +155,31 @@ def assign_to(self, git_strarray):
153155
git_strarray.count = len(self.__strings)
154156

155157

156-
class GenericIterator:
158+
T = TypeVar('T')
159+
U = TypeVar('U', covariant=True)
160+
161+
162+
class SequenceProtocol(Protocol[U]):
163+
def __len__(self) -> int: ...
164+
def __getitem__(self, index: int) -> U: ...
165+
166+
167+
class GenericIterator(Generic[T]):
157168
"""Helper to easily implement an iterator.
158169
159170
The constructor gets a container which must implement __len__ and
160171
__getitem__
161172
"""
162173

163-
def __init__(self, container):
174+
def __init__(self, container: SequenceProtocol[T]) -> None:
164175
self.container = container
165176
self.length = len(container)
166177
self.idx = 0
167178

168-
def __iter__(self):
179+
def __iter__(self) -> Iterator[T]:
169180
return self
170181

171-
def __next__(self):
182+
def __next__(self) -> T:
172183
idx = self.idx
173184
if idx >= self.length:
174185
raise StopIteration

test/test_blame.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import pytest
2929

30-
from pygit2 import Signature, Oid
30+
from pygit2 import Signature, Oid, Repository
3131
from pygit2.enums import BlameFlag
3232

3333

@@ -61,7 +61,7 @@
6161
]
6262

6363

64-
def test_blame_index(testrepo):
64+
def test_blame_index(testrepo: Repository) -> None:
6565
blame = testrepo.blame(PATH)
6666

6767
assert len(blame) == 3
@@ -78,7 +78,7 @@ def test_blame_index(testrepo):
7878
assert HUNKS[i][3] == hunk.boundary
7979

8080

81-
def test_blame_flags(blameflagsrepo):
81+
def test_blame_flags(blameflagsrepo: Repository) -> None:
8282
blame = blameflagsrepo.blame(PATH, flags=BlameFlag.IGNORE_WHITESPACE)
8383

8484
assert len(blame) == 3
@@ -95,7 +95,7 @@ def test_blame_flags(blameflagsrepo):
9595
assert HUNKS[i][3] == hunk.boundary
9696

9797

98-
def test_blame_with_invalid_index(testrepo):
98+
def test_blame_with_invalid_index(testrepo: Repository) -> None:
9999
blame = testrepo.blame(PATH)
100100

101101
def test():
@@ -106,7 +106,7 @@ def test():
106106
test()
107107

108108

109-
def test_blame_for_line(testrepo):
109+
def test_blame_for_line(testrepo: Repository) -> None:
110110
blame = testrepo.blame(PATH)
111111

112112
for i, line in zip(range(0, 2), range(1, 3)):
@@ -123,19 +123,18 @@ def test_blame_for_line(testrepo):
123123
assert HUNKS[i][3] == hunk.boundary
124124

125125

126-
def test_blame_with_invalid_line(testrepo):
126+
def test_blame_with_invalid_line(testrepo: Repository) -> None:
127127
blame = testrepo.blame(PATH)
128128

129-
def test():
129+
with pytest.raises(IndexError):
130130
blame.for_line(0)
131+
with pytest.raises(IndexError):
131132
blame.for_line(100000)
132-
blame.for_line(-1)
133-
134133
with pytest.raises(IndexError):
135-
test()
134+
blame.for_line(-1)
136135

137136

138-
def test_blame_newest(testrepo):
137+
def test_blame_newest(testrepo: Repository) -> None:
139138
revs = [
140139
('master^2', 3),
141140
('master^2^', 2),

0 commit comments

Comments
 (0)