Skip to content

Commit ae13c6d

Browse files
committed
Add types to refs/log.py
1 parent 1dd4596 commit ae13c6d

File tree

1 file changed

+86
-54
lines changed

1 file changed

+86
-54
lines changed

‎git/refs/log.py

+86-54
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
2+
from mmap import mmap
13
import re
2-
import time
4+
import time as _time
35

46
from git.compat import defenc
57
from git.objects.util import (
@@ -20,20 +22,33 @@
2022
import os.path as osp
2123

2224

25+
# typing ------------------------------------------------------------------
26+
27+
from typing import Iterator, List, Tuple, Union, TYPE_CHECKING
28+
29+
from git.types import PathLike
30+
31+
if TYPE_CHECKING:
32+
from git.refs import SymbolicReference
33+
from io import BytesIO
34+
from git.config import GitConfigParser, SectionConstraint # NOQA
35+
36+
# ------------------------------------------------------------------------------
37+
2338
__all__ = ["RefLog", "RefLogEntry"]
2439

2540

26-
class RefLogEntry(tuple):
41+
class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
2742

2843
"""Named tuple allowing easy access to the revlog data fields"""
2944
_re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$')
3045
__slots__ = ()
3146

32-
def __repr__(self):
47+
def __repr__(self) -> str:
3348
"""Representation of ourselves in git reflog format"""
3449
return self.format()
3550

36-
def format(self):
51+
def format(self) -> str:
3752
""":return: a string suitable to be placed in a reflog file"""
3853
act = self.actor
3954
time = self.time
@@ -46,35 +61,36 @@ def format(self):
4661
self.message)
4762

4863
@property
49-
def oldhexsha(self):
64+
def oldhexsha(self) -> str:
5065
"""The hexsha to the commit the ref pointed to before the change"""
5166
return self[0]
5267

5368
@property
54-
def newhexsha(self):
69+
def newhexsha(self) -> str:
5570
"""The hexsha to the commit the ref now points to, after the change"""
5671
return self[1]
5772

5873
@property
59-
def actor(self):
74+
def actor(self) -> Actor:
6075
"""Actor instance, providing access"""
6176
return self[2]
6277

6378
@property
64-
def time(self):
79+
def time(self) -> Tuple[int, int]:
6580
"""time as tuple:
6681
6782
* [0] = int(time)
6883
* [1] = int(timezone_offset) in time.altzone format """
6984
return self[3]
7085

7186
@property
72-
def message(self):
87+
def message(self) -> str:
7388
"""Message describing the operation that acted on the reference"""
7489
return self[4]
7590

7691
@classmethod
77-
def new(cls, oldhexsha, newhexsha, actor, time, tz_offset, message): # skipcq: PYL-W0621
92+
def new(cls, oldhexsha: str, newhexsha: str, actor: Actor, time: int, tz_offset: int, message: str
93+
) -> 'RefLogEntry': # skipcq: PYL-W0621
7894
""":return: New instance of a RefLogEntry"""
7995
if not isinstance(actor, Actor):
8096
raise ValueError("Need actor instance, got %s" % actor)
@@ -111,14 +127,15 @@ def from_line(cls, line: bytes) -> 'RefLogEntry':
111127
# END handle missing end brace
112128

113129
actor = Actor._from_string(info[82:email_end + 1])
114-
time, tz_offset = parse_date(info[email_end + 2:]) # skipcq: PYL-W0621
130+
time, tz_offset = parse_date(
131+
info[email_end + 2:]) # skipcq: PYL-W0621
115132

116133
return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg))
117134

118135

119-
class RefLog(list, Serializable):
136+
class RefLog(List[RefLogEntry], Serializable):
120137

121-
"""A reflog contains reflog entries, each of which defines a certain state
138+
"""A reflog contains RefLogEntrys, each of which defines a certain state
122139
of the head in question. Custom query methods allow to retrieve log entries
123140
by date or by other criteria.
124141
@@ -127,11 +144,11 @@ class RefLog(list, Serializable):
127144

128145
__slots__ = ('_path', )
129146

130-
def __new__(cls, filepath=None):
147+
def __new__(cls, filepath: Union[PathLike, None] = None) -> 'RefLog':
131148
inst = super(RefLog, cls).__new__(cls)
132149
return inst
133150

134-
def __init__(self, filepath=None):
151+
def __init__(self, filepath: Union[PathLike, None] = None):
135152
"""Initialize this instance with an optional filepath, from which we will
136153
initialize our data. The path is also used to write changes back using
137154
the write() method"""
@@ -142,7 +159,8 @@ def __init__(self, filepath=None):
142159

143160
def _read_from_file(self):
144161
try:
145-
fmap = file_contents_ro_filepath(self._path, stream=True, allow_mmap=True)
162+
fmap = file_contents_ro_filepath(
163+
self._path, stream=True, allow_mmap=True)
146164
except OSError:
147165
# it is possible and allowed that the file doesn't exist !
148166
return
@@ -154,10 +172,10 @@ def _read_from_file(self):
154172
fmap.close()
155173
# END handle closing of handle
156174

157-
#{ Interface
175+
# { Interface
158176

159177
@classmethod
160-
def from_file(cls, filepath):
178+
def from_file(cls, filepath: PathLike) -> 'RefLog':
161179
"""
162180
:return: a new RefLog instance containing all entries from the reflog
163181
at the given filepath
@@ -166,7 +184,7 @@ def from_file(cls, filepath):
166184
return cls(filepath)
167185

168186
@classmethod
169-
def path(cls, ref):
187+
def path(cls, ref: 'SymbolicReference') -> str:
170188
"""
171189
:return: string to absolute path at which the reflog of the given ref
172190
instance would be found. The path is not guaranteed to point to a valid
@@ -175,28 +193,34 @@ def path(cls, ref):
175193
return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path))
176194

177195
@classmethod
178-
def iter_entries(cls, stream):
196+
def iter_entries(cls, stream: Union[str, 'BytesIO', mmap]) -> Iterator[RefLogEntry]:
179197
"""
180198
:return: Iterator yielding RefLogEntry instances, one for each line read
181199
sfrom the given stream.
182200
:param stream: file-like object containing the revlog in its native format
183-
or basestring instance pointing to a file to read"""
201+
or string instance pointing to a file to read"""
184202
new_entry = RefLogEntry.from_line
185203
if isinstance(stream, str):
186-
stream = file_contents_ro_filepath(stream)
204+
# default args return mmap on py>3
205+
_stream = file_contents_ro_filepath(stream)
206+
assert isinstance(_stream, mmap)
207+
else:
208+
_stream = stream
187209
# END handle stream type
188210
while True:
189-
line = stream.readline()
211+
line = _stream.readline()
190212
if not line:
191213
return
192214
yield new_entry(line.strip())
193215
# END endless loop
194-
stream.close()
195216

196217
@classmethod
197-
def entry_at(cls, filepath, index):
198-
""":return: RefLogEntry at the given index
218+
def entry_at(cls, filepath: PathLike, index: int) -> 'RefLogEntry':
219+
"""
220+
:return: RefLogEntry at the given index
221+
199222
:param filepath: full path to the index file from which to read the entry
223+
200224
:param index: python list compatible index, i.e. it may be negative to
201225
specify an entry counted from the end of the list
202226
@@ -210,21 +234,19 @@ def entry_at(cls, filepath, index):
210234
if index < 0:
211235
return RefLogEntry.from_line(fp.readlines()[index].strip())
212236
# read until index is reached
237+
213238
for i in range(index + 1):
214239
line = fp.readline()
215240
if not line:
216-
break
241+
raise IndexError(
242+
f"Index file ended at line {i+1}, before given index was reached")
217243
# END abort on eof
218244
# END handle runup
219245

220-
if i != index or not line: # skipcq:PYL-W0631
221-
raise IndexError
222-
# END handle exception
223-
224246
return RefLogEntry.from_line(line.strip())
225247
# END handle index
226248

227-
def to_file(self, filepath):
249+
def to_file(self, filepath: PathLike) -> None:
228250
"""Write the contents of the reflog instance to a file at the given filepath.
229251
:param filepath: path to file, parent directories are assumed to exist"""
230252
lfd = LockedFD(filepath)
@@ -241,65 +263,75 @@ def to_file(self, filepath):
241263
# END handle change
242264

243265
@classmethod
244-
def append_entry(cls, config_reader, filepath, oldbinsha, newbinsha, message):
266+
def append_entry(cls, config_reader: Union[Actor, 'GitConfigParser', 'SectionConstraint', None],
267+
filepath: PathLike, oldbinsha: bytes, newbinsha: bytes, message: str,
268+
write: bool = True) -> 'RefLogEntry':
245269
"""Append a new log entry to the revlog at filepath.
246270
247271
:param config_reader: configuration reader of the repository - used to obtain
248-
user information. May also be an Actor instance identifying the committer directly.
249-
May also be None
272+
user information. May also be an Actor instance identifying the committer directly or None.
250273
:param filepath: full path to the log file
251274
:param oldbinsha: binary sha of the previous commit
252275
:param newbinsha: binary sha of the current commit
253276
:param message: message describing the change to the reference
254277
:param write: If True, the changes will be written right away. Otherwise
255278
the change will not be written
279+
256280
:return: RefLogEntry objects which was appended to the log
281+
257282
:note: As we are append-only, concurrent access is not a problem as we
258283
do not interfere with readers."""
284+
259285
if len(oldbinsha) != 20 or len(newbinsha) != 20:
260286
raise ValueError("Shas need to be given in binary format")
261287
# END handle sha type
262288
assure_directory_exists(filepath, is_file=True)
263289
first_line = message.split('\n')[0]
264-
committer = isinstance(config_reader, Actor) and config_reader or Actor.committer(config_reader)
290+
if isinstance(config_reader, Actor):
291+
committer = config_reader # mypy thinks this is Actor | Gitconfigparser, but why?
292+
else:
293+
committer = Actor.committer(config_reader)
265294
entry = RefLogEntry((
266295
bin_to_hex(oldbinsha).decode('ascii'),
267296
bin_to_hex(newbinsha).decode('ascii'),
268-
committer, (int(time.time()), time.altzone), first_line
297+
committer, (int(_time.time()), _time.altzone), first_line
269298
))
270299

271-
lf = LockFile(filepath)
272-
lf._obtain_lock_or_raise()
273-
fd = open(filepath, 'ab')
274-
try:
275-
fd.write(entry.format().encode(defenc))
276-
finally:
277-
fd.close()
278-
lf._release_lock()
279-
# END handle write operation
280-
300+
if write:
301+
lf = LockFile(filepath)
302+
lf._obtain_lock_or_raise()
303+
fd = open(filepath, 'ab')
304+
try:
305+
fd.write(entry.format().encode(defenc))
306+
finally:
307+
fd.close()
308+
lf._release_lock()
309+
# END handle write operation
281310
return entry
282311

283-
def write(self):
312+
def write(self) -> 'RefLog':
284313
"""Write this instance's data to the file we are originating from
285314
:return: self"""
286315
if self._path is None:
287-
raise ValueError("Instance was not initialized with a path, use to_file(...) instead")
316+
raise ValueError(
317+
"Instance was not initialized with a path, use to_file(...) instead")
288318
# END assert path
289319
self.to_file(self._path)
290320
return self
291321

292-
#} END interface
322+
# } END interface
293323

294-
#{ Serializable Interface
295-
def _serialize(self, stream):
324+
# { Serializable Interface
325+
def _serialize(self, stream: 'BytesIO') -> 'RefLog':
296326
write = stream.write
297327

298328
# write all entries
299329
for e in self:
300330
write(e.format().encode(defenc))
301331
# END for each entry
332+
return self
302333

303-
def _deserialize(self, stream):
334+
def _deserialize(self, stream: 'BytesIO') -> 'RefLog':
304335
self.extend(self.iter_entries(stream))
305-
#} END serializable interface
336+
# } END serializable interface
337+
return self

0 commit comments

Comments
 (0)