Skip to content

Commit 9523033

Browse files
authored
Merge pull request #1285 from Yobmod/main
Finish initial typing of Index and Submodule
2 parents 0a6d9d6 + 94c6652 commit 9523033

20 files changed

+504
-311
lines changed

git/cmd.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ def execute(self,
831831
except cmd_not_found_exception as err:
832832
raise GitCommandNotFound(redacted_command, err) from err
833833
else:
834-
proc = cast(Popen, proc)
834+
# replace with a typeguard for Popen[bytes]?
835835
proc.stdout = cast(BinaryIO, proc.stdout)
836836
proc.stderr = cast(BinaryIO, proc.stderr)
837837

git/config.py

+23-20
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@
3131

3232
# typing-------------------------------------------------------
3333

34-
from typing import Any, Callable, IO, List, Dict, Sequence, TYPE_CHECKING, Tuple, Union, cast, overload
34+
from typing import (Any, Callable, IO, List, Dict, Sequence,
35+
TYPE_CHECKING, Tuple, Union, cast, overload)
3536

36-
from git.types import Literal, Lit_config_levels, PathLike, TBD
37+
from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, TBD, assert_never, is_config_level
3738

3839
if TYPE_CHECKING:
3940
from git.repo.base import Repo
41+
from io import BytesIO
4042

4143
# -------------------------------------------------------------
4244

@@ -48,8 +50,10 @@
4850

4951
# invariants
5052
# represents the configuration level of a configuration file
51-
CONFIG_LEVELS = ("system", "user", "global", "repository"
52-
) # type: Tuple[Literal['system'], Literal['user'], Literal['global'], Literal['repository']]
53+
54+
55+
CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
56+
5357

5458
# Section pattern to detect conditional includes.
5559
# https://git-scm.com/docs/git-config#_conditional_includes
@@ -229,8 +233,9 @@ def get_config_path(config_level: Lit_config_levels) -> str:
229233
return osp.normpath(osp.expanduser("~/.gitconfig"))
230234
elif config_level == "repository":
231235
raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path")
232-
233-
raise ValueError("Invalid configuration level: %r" % config_level)
236+
else:
237+
# Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs
238+
assert_never(config_level, ValueError(f"Invalid configuration level: {config_level!r}"))
234239

235240

236241
class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): # type: ignore ## mypy does not understand dynamic class creation # noqa: E501
@@ -271,7 +276,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
271276
# list of RawConfigParser methods able to change the instance
272277
_mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
273278

274-
def __init__(self, file_or_files: Union[None, PathLike, IO, Sequence[Union[PathLike, IO]]] = None,
279+
def __init__(self, file_or_files: Union[None, PathLike, 'BytesIO', Sequence[Union[PathLike, 'BytesIO']]] = None,
275280
read_only: bool = True, merge_includes: bool = True,
276281
config_level: Union[Lit_config_levels, None] = None,
277282
repo: Union['Repo', None] = None) -> None:
@@ -300,13 +305,13 @@ def __init__(self, file_or_files: Union[None, PathLike, IO, Sequence[Union[PathL
300305
self._proxies = self._dict()
301306

302307
if file_or_files is not None:
303-
self._file_or_files = file_or_files # type: Union[PathLike, IO, Sequence[Union[PathLike, IO]]]
308+
self._file_or_files: Union[PathLike, 'BytesIO', Sequence[Union[PathLike, 'BytesIO']]] = file_or_files
304309
else:
305310
if config_level is None:
306311
if read_only:
307-
self._file_or_files = [get_config_path(f) # type: ignore
308-
for f in CONFIG_LEVELS # Can type f properly when 3.5 dropped
309-
if f != 'repository']
312+
self._file_or_files = [get_config_path(f)
313+
for f in CONFIG_LEVELS
314+
if is_config_level(f) and f != 'repository']
310315
else:
311316
raise ValueError("No configuration level or configuration files specified")
312317
else:
@@ -323,15 +328,13 @@ def __init__(self, file_or_files: Union[None, PathLike, IO, Sequence[Union[PathL
323328
def _acquire_lock(self) -> None:
324329
if not self._read_only:
325330
if not self._lock:
326-
if isinstance(self._file_or_files, (tuple, list)):
327-
raise ValueError(
328-
"Write-ConfigParsers can operate on a single file only, multiple files have been passed")
329-
# END single file check
330-
331331
if isinstance(self._file_or_files, (str, os.PathLike)):
332332
file_or_files = self._file_or_files
333+
elif isinstance(self._file_or_files, (tuple, list, Sequence)):
334+
raise ValueError(
335+
"Write-ConfigParsers can operate on a single file only, multiple files have been passed")
333336
else:
334-
file_or_files = cast(IO, self._file_or_files).name
337+
file_or_files = self._file_or_files.name
335338

336339
# END get filename from handle/stream
337340
# initialize lock base - we want to write
@@ -649,7 +652,7 @@ def write(self) -> None:
649652
a file lock"""
650653
self._assure_writable("write")
651654
if not self._dirty:
652-
return
655+
return None
653656

654657
if isinstance(self._file_or_files, (list, tuple)):
655658
raise AssertionError("Cannot write back if there is not exactly a single file to write to, have %i files"
@@ -665,7 +668,7 @@ def write(self) -> None:
665668
fp = self._file_or_files
666669

667670
# we have a physical file on disk, so get a lock
668-
is_file_lock = isinstance(fp, (str, IOBase)) # can't use Pathlike until 3.5 dropped
671+
is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # can't use Pathlike until 3.5 dropped
669672
if is_file_lock and self._lock is not None: # else raise Error?
670673
self._lock._obtain_lock()
671674

@@ -674,7 +677,7 @@ def write(self) -> None:
674677
with open(fp, "wb") as fp_open:
675678
self._write(fp_open)
676679
else:
677-
fp = cast(IO, fp)
680+
fp = cast('BytesIO', fp)
678681
fp.seek(0)
679682
# make sure we do not overwrite into an existing file
680683
if hasattr(fp, 'truncate'):

git/diff.py

+58-43
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@
1515

1616
# typing ------------------------------------------------------------------
1717

18-
from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING
19-
from git.types import PathLike, TBD, Literal
18+
from typing import Any, Iterator, List, Match, Optional, Tuple, Type, TypeVar, Union, TYPE_CHECKING
19+
from git.types import PathLike, TBD, Literal, TypeGuard
2020

2121
if TYPE_CHECKING:
2222
from .objects.tree import Tree
23+
from .objects import Commit
2324
from git.repo.base import Repo
24-
25+
from git.objects.base import IndexObject
2526
from subprocess import Popen
2627

27-
Lit_change_type = Literal['A', 'D', 'M', 'R', 'T']
28+
Lit_change_type = Literal['A', 'D', 'C', 'M', 'R', 'T', 'U']
29+
30+
31+
def is_change_type(inp: str) -> TypeGuard[Lit_change_type]:
32+
# return True
33+
return inp in ['A', 'D', 'C', 'M', 'R', 'T', 'U']
2834

2935
# ------------------------------------------------------------------------
3036

37+
3138
__all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE')
3239

3340
# Special object to compare against the empty tree in diffs
@@ -75,15 +82,16 @@ class Diffable(object):
7582
class Index(object):
7683
pass
7784

78-
def _process_diff_args(self, args: List[Union[str, 'Diffable', object]]) -> List[Union[str, 'Diffable', object]]:
85+
def _process_diff_args(self, args: List[Union[str, 'Diffable', Type['Diffable.Index'], object]]
86+
) -> List[Union[str, 'Diffable', Type['Diffable.Index'], object]]:
7987
"""
8088
:return:
8189
possibly altered version of the given args list.
8290
Method is called right before git command execution.
8391
Subclasses can use it to alter the behaviour of the superclass"""
8492
return args
8593

86-
def diff(self, other: Union[Type[Index], Type['Tree'], object, None, str] = Index,
94+
def diff(self, other: Union[Type['Index'], 'Tree', 'Commit', None, str, object] = Index,
8795
paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
8896
create_patch: bool = False, **kwargs: Any) -> 'DiffIndex':
8997
"""Creates diffs between two items being trees, trees and index or an
@@ -116,7 +124,7 @@ def diff(self, other: Union[Type[Index], Type['Tree'], object, None, str] = Inde
116124
:note:
117125
On a bare repository, 'other' needs to be provided as Index or as
118126
as Tree/Commit, or a git command error will occur"""
119-
args = [] # type: List[Union[str, Diffable, object]]
127+
args: List[Union[PathLike, Diffable, Type['Diffable.Index'], object]] = []
120128
args.append("--abbrev=40") # we need full shas
121129
args.append("--full-index") # get full index paths, not only filenames
122130

@@ -134,8 +142,8 @@ def diff(self, other: Union[Type[Index], Type['Tree'], object, None, str] = Inde
134142
if paths is not None and not isinstance(paths, (tuple, list)):
135143
paths = [paths]
136144

137-
if hasattr(self, 'repo'): # else raise Error?
138-
self.repo = self.repo # type: 'Repo'
145+
if hasattr(self, 'Has_Repo'):
146+
self.repo: Repo = self.repo
139147

140148
diff_cmd = self.repo.git.diff
141149
if other is self.Index:
@@ -169,7 +177,10 @@ def diff(self, other: Union[Type[Index], Type['Tree'], object, None, str] = Inde
169177
return index
170178

171179

172-
class DiffIndex(list):
180+
T_Diff = TypeVar('T_Diff', bound='Diff')
181+
182+
183+
class DiffIndex(List[T_Diff]):
173184

174185
"""Implements an Index for diffs, allowing a list of Diffs to be queried by
175186
the diff properties.
@@ -183,7 +194,7 @@ class DiffIndex(list):
183194
# T = Changed in the type
184195
change_type = ("A", "C", "D", "R", "M", "T")
185196

186-
def iter_change_type(self, change_type: Lit_change_type) -> Iterator['Diff']:
197+
def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
187198
"""
188199
:return:
189200
iterator yielding Diff instances that match the given change_type
@@ -200,19 +211,19 @@ def iter_change_type(self, change_type: Lit_change_type) -> Iterator['Diff']:
200211
if change_type not in self.change_type:
201212
raise ValueError("Invalid change type: %s" % change_type)
202213

203-
for diff in self: # type: 'Diff'
204-
if diff.change_type == change_type:
205-
yield diff
206-
elif change_type == "A" and diff.new_file:
207-
yield diff
208-
elif change_type == "D" and diff.deleted_file:
209-
yield diff
210-
elif change_type == "C" and diff.copied_file:
211-
yield diff
212-
elif change_type == "R" and diff.renamed:
213-
yield diff
214-
elif change_type == "M" and diff.a_blob and diff.b_blob and diff.a_blob != diff.b_blob:
215-
yield diff
214+
for diffidx in self:
215+
if diffidx.change_type == change_type:
216+
yield diffidx
217+
elif change_type == "A" and diffidx.new_file:
218+
yield diffidx
219+
elif change_type == "D" and diffidx.deleted_file:
220+
yield diffidx
221+
elif change_type == "C" and diffidx.copied_file:
222+
yield diffidx
223+
elif change_type == "R" and diffidx.renamed:
224+
yield diffidx
225+
elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob:
226+
yield diffidx
216227
# END for each diff
217228

218229

@@ -281,7 +292,7 @@ def __init__(self, repo: 'Repo',
281292
a_mode: Union[bytes, str, None], b_mode: Union[bytes, str, None],
282293
new_file: bool, deleted_file: bool, copied_file: bool,
283294
raw_rename_from: Optional[bytes], raw_rename_to: Optional[bytes],
284-
diff: Union[str, bytes, None], change_type: Optional[str], score: Optional[int]) -> None:
295+
diff: Union[str, bytes, None], change_type: Optional[Lit_change_type], score: Optional[int]) -> None:
285296

286297
assert a_rawpath is None or isinstance(a_rawpath, bytes)
287298
assert b_rawpath is None or isinstance(b_rawpath, bytes)
@@ -300,19 +311,21 @@ def __init__(self, repo: 'Repo',
300311
repo = submodule.module()
301312
break
302313

314+
self.a_blob: Union['IndexObject', None]
303315
if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA:
304316
self.a_blob = None
305317
else:
306318
self.a_blob = Blob(repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path)
307319

320+
self.b_blob: Union['IndexObject', None]
308321
if b_blob_id is None or b_blob_id == self.NULL_HEX_SHA:
309322
self.b_blob = None
310323
else:
311324
self.b_blob = Blob(repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path)
312325

313-
self.new_file = new_file
314-
self.deleted_file = deleted_file
315-
self.copied_file = copied_file
326+
self.new_file: bool = new_file
327+
self.deleted_file: bool = deleted_file
328+
self.copied_file: bool = copied_file
316329

317330
# be clear and use None instead of empty strings
318331
assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
@@ -321,7 +334,7 @@ def __init__(self, repo: 'Repo',
321334
self.raw_rename_to = raw_rename_to or None
322335

323336
self.diff = diff
324-
self.change_type = change_type
337+
self.change_type: Union[Lit_change_type, None] = change_type
325338
self.score = score
326339

327340
def __eq__(self, other: object) -> bool:
@@ -386,36 +399,36 @@ def __str__(self) -> str:
386399
# end
387400
return res
388401

389-
@property
402+
@ property
390403
def a_path(self) -> Optional[str]:
391404
return self.a_rawpath.decode(defenc, 'replace') if self.a_rawpath else None
392405

393-
@property
406+
@ property
394407
def b_path(self) -> Optional[str]:
395408
return self.b_rawpath.decode(defenc, 'replace') if self.b_rawpath else None
396409

397-
@property
410+
@ property
398411
def rename_from(self) -> Optional[str]:
399412
return self.raw_rename_from.decode(defenc, 'replace') if self.raw_rename_from else None
400413

401-
@property
414+
@ property
402415
def rename_to(self) -> Optional[str]:
403416
return self.raw_rename_to.decode(defenc, 'replace') if self.raw_rename_to else None
404417

405-
@property
418+
@ property
406419
def renamed(self) -> bool:
407420
""":returns: True if the blob of our diff has been renamed
408421
:note: This property is deprecated, please use ``renamed_file`` instead.
409422
"""
410423
return self.renamed_file
411424

412-
@property
425+
@ property
413426
def renamed_file(self) -> bool:
414427
""":returns: True if the blob of our diff has been renamed
415428
"""
416429
return self.rename_from != self.rename_to
417430

418-
@classmethod
431+
@ classmethod
419432
def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]:
420433
if path_match:
421434
return decode_path(path_match)
@@ -428,7 +441,7 @@ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_m
428441

429442
return None
430443

431-
@classmethod
444+
@ classmethod
432445
def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex:
433446
"""Create a new DiffIndex from the given text which must be in patch format
434447
:param repo: is the repository we are operating on - it is required
@@ -441,7 +454,7 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex:
441454

442455
# for now, we have to bake the stream
443456
text = b''.join(text_list)
444-
index = DiffIndex()
457+
index: 'DiffIndex' = DiffIndex()
445458
previous_header = None
446459
header = None
447460
a_path, b_path = None, None # for mypy
@@ -491,19 +504,21 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex:
491504

492505
return index
493506

494-
@staticmethod
507+
@ staticmethod
495508
def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> None:
496509
lines = lines_bytes.decode(defenc)
497510

498511
for line in lines.split(':')[1:]:
499512
meta, _, path = line.partition('\x00')
500513
path = path.rstrip('\x00')
501-
a_blob_id, b_blob_id = None, None # Type: Optional[str]
514+
a_blob_id: Optional[str]
515+
b_blob_id: Optional[str]
502516
old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
503517
# Change type can be R100
504518
# R: status letter
505519
# 100: score (in case of copy and rename)
506-
change_type = _change_type[0]
520+
assert is_change_type(_change_type[0]), f"Unexpected value for change_type received: {_change_type[0]}"
521+
change_type: Lit_change_type = _change_type[0]
507522
score_str = ''.join(_change_type[1:])
508523
score = int(score_str) if score_str.isdigit() else None
509524
path = path.strip()
@@ -543,14 +558,14 @@ def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> Non
543558
'', change_type, score)
544559
index.append(diff)
545560

546-
@classmethod
561+
@ classmethod
547562
def _index_from_raw_format(cls, repo: 'Repo', proc: 'Popen') -> 'DiffIndex':
548563
"""Create a new DiffIndex from the given stream which must be in raw format.
549564
:return: git.DiffIndex"""
550565
# handles
551566
# :100644 100644 687099101... 37c5e30c8... M .gitignore
552567

553-
index = DiffIndex()
568+
index: 'DiffIndex' = DiffIndex()
554569
handle_process_output(proc, lambda byt: cls._handle_diff_line(byt, repo, index),
555570
None, finalize_process, decode_streams=False)
556571

0 commit comments

Comments
 (0)