Skip to content

Add initial types to object, and fix CI #1271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jun 18, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# UNUSED, only for reference. If adjustments are needed, please see github actions
language: python
python:
<<<<<<< HEAD
- "3.4"
=======
>>>>>>> b0f79c58ad919e90261d1e332df79a4ad0bc40de
- "3.6"
- "3.7"
- "3.8"
3 changes: 1 addition & 2 deletions git/cmd.py
Original file line number Diff line number Diff line change
@@ -148,7 +148,6 @@ def dashify(string: str) -> str:


def slots_to_dict(self, exclude: Sequence[str] = ()) -> Dict[str, Any]:
# annotate self.__slots__ as Tuple[str, ...] once 3.5 dropped
return {s: getattr(self, s) for s in self.__slots__ if s not in exclude}


@@ -460,7 +459,7 @@ class CatFileContentStream(object):
If not all data is read to the end of the objects's lifetime, we read the
rest to assure the underlying stream continues to work"""

__slots__ = ('_stream', '_nbr', '_size')
__slots__: Tuple[str, ...] = ('_stream', '_nbr', '_size')

def __init__(self, size: int, stream: IO[bytes]) -> None:
self._stream = stream
4 changes: 2 additions & 2 deletions git/diff.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
# typing ------------------------------------------------------------------

from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING
from git.types import PathLike, TBD, Final, Literal
from git.types import PathLike, TBD, Literal

if TYPE_CHECKING:
from .objects.tree import Tree
@@ -31,7 +31,7 @@
__all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE')

# Special object to compare against the empty tree in diffs
NULL_TREE = object() # type: Final[object]
NULL_TREE = object()

_octal_byte_re = re.compile(b'\\\\([0-9]{3})')

2 changes: 1 addition & 1 deletion git/index/fun.py
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None:
# end handle return code


def stat_mode_to_index_mode(mode):
def stat_mode_to_index_mode(mode: int) -> int:
"""Convert the given mode from a stat call to the corresponding index mode
and return it"""
if S_ISLNK(mode): # symlinks
65 changes: 44 additions & 21 deletions git/objects/base.py
Original file line number Diff line number Diff line change
@@ -3,16 +3,34 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php

from git.exc import WorkTreeRepositoryUnsupported
from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex

import gitdb.typ as dbtyp
import os.path as osp
from typing import Optional # noqa: F401 unused import

from .util import get_object_type_by_name


_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutal git object type %r"
# typing ------------------------------------------------------------------

from typing import Any, TYPE_CHECKING, Optional, Union

from git.types import PathLike

if TYPE_CHECKING:
from git.repo import Repo
from gitdb.base import OStream
from .tree import Tree
from .blob import Blob
from .tag import TagObject
from .commit import Commit

# --------------------------------------------------------------------------


_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutual git object type %r"

__all__ = ("Object", "IndexObject")

@@ -27,7 +45,7 @@ class Object(LazyMixin):
__slots__ = ("repo", "binsha", "size")
type = None # type: Optional[str] # to be set by subclass

def __init__(self, repo, binsha):
def __init__(self, repo: 'Repo', binsha: bytes):
"""Initialize an object by identifying it by its binary sha.
All keyword arguments will be set on demand if None.

@@ -40,7 +58,7 @@ def __init__(self, repo, binsha):
assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (binsha, len(binsha))

@classmethod
def new(cls, repo, id): # @ReservedAssignment
def new(cls, repo: 'Repo', id): # @ReservedAssignment
"""
:return: New Object instance of a type appropriate to the object type behind
id. The id of the newly created object will be a binsha even though
@@ -53,7 +71,7 @@ def new(cls, repo, id): # @ReservedAssignment
return repo.rev_parse(str(id))

@classmethod
def new_from_sha(cls, repo, sha1):
def new_from_sha(cls, repo: 'Repo', sha1: bytes) -> Union['Commit', 'TagObject', 'Tree', 'Blob']:
"""
:return: new object instance of a type appropriate to represent the given
binary sha1
@@ -67,52 +85,52 @@ def new_from_sha(cls, repo, sha1):
inst.size = oinfo.size
return inst

def _set_cache_(self, attr):
def _set_cache_(self, attr: str) -> None:
"""Retrieve object information"""
if attr == "size":
oinfo = self.repo.odb.info(self.binsha)
self.size = oinfo.size
self.size = oinfo.size # type: int
# assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type)
else:
super(Object, self)._set_cache_(attr)

def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
""":return: True if the objects have the same SHA1"""
if not hasattr(other, 'binsha'):
return False
return self.binsha == other.binsha

def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
""":return: True if the objects do not have the same SHA1 """
if not hasattr(other, 'binsha'):
return True
return self.binsha != other.binsha

def __hash__(self):
def __hash__(self) -> int:
""":return: Hash of our id allowing objects to be used in dicts and sets"""
return hash(self.binsha)

def __str__(self):
def __str__(self) -> str:
""":return: string of our SHA1 as understood by all git commands"""
return self.hexsha

def __repr__(self):
def __repr__(self) -> str:
""":return: string with pythonic representation of our object"""
return '<git.%s "%s">' % (self.__class__.__name__, self.hexsha)

@property
def hexsha(self):
def hexsha(self) -> str:
""":return: 40 byte hex version of our 20 byte binary sha"""
# b2a_hex produces bytes
return bin_to_hex(self.binsha).decode('ascii')

@property
def data_stream(self):
def data_stream(self) -> 'OStream':
""" :return: File Object compatible stream to the uncompressed raw data of the object
:note: returned streams must be read in order"""
return self.repo.odb.stream(self.binsha)

def stream_data(self, ostream):
def stream_data(self, ostream: 'OStream') -> 'Object':
"""Writes our data directly to the given output stream
:param ostream: File object compatible stream object.
:return: self"""
@@ -130,7 +148,9 @@ class IndexObject(Object):
# for compatibility with iterable lists
_id_attribute_ = 'path'

def __init__(self, repo, binsha, mode=None, path=None):
def __init__(self,
repo: 'Repo', binsha: bytes, mode: Union[None, int] = None, path: Union[None, PathLike] = None
) -> None:
"""Initialize a newly instanced IndexObject

:param repo: is the Repo we are located in
@@ -150,14 +170,14 @@ def __init__(self, repo, binsha, mode=None, path=None):
if path is not None:
self.path = path

def __hash__(self):
def __hash__(self) -> int:
"""
:return:
Hash of our path as index items are uniquely identifiable by path, not
by their data !"""
return hash(self.path)

def _set_cache_(self, attr):
def _set_cache_(self, attr: str) -> None:
if attr in IndexObject.__slots__:
# they cannot be retrieved lateron ( not without searching for them )
raise AttributeError(
@@ -168,16 +188,19 @@ def _set_cache_(self, attr):
# END handle slot attribute

@property
def name(self):
def name(self) -> str:
""":return: Name portion of the path, effectively being the basename"""
return osp.basename(self.path)

@property
def abspath(self):
def abspath(self) -> PathLike:
"""
:return:
Absolute path to this index object in the file system ( as opposed to the
.path field which is a path relative to the git repository ).

The returned path will be native to the system and contains '\' on windows. """
return join_path_native(self.repo.working_tree_dir, self.path)
if self.repo.working_tree_dir is not None:
return join_path_native(self.repo.working_tree_dir, self.path)
else:
raise WorkTreeRepositoryUnsupported("Working_tree_dir was None or empty")
4 changes: 2 additions & 2 deletions git/objects/blob.py
Original file line number Diff line number Diff line change
@@ -23,11 +23,11 @@ class Blob(base.IndexObject):
__slots__ = ()

@property
def mime_type(self):
def mime_type(self) -> str:
"""
:return: String describing the mime type of this file (based on the filename)
:note: Defaults to 'text/plain' in case the actual file type is unknown. """
guesses = None
if self.path:
guesses = guess_type(self.path)
guesses = guess_type(str(self.path))
return guesses and guesses[0] or self.DEFAULT_MIME_TYPE
20 changes: 13 additions & 7 deletions git/objects/commit.py
Original file line number Diff line number Diff line change
@@ -36,6 +36,11 @@
from io import BytesIO
import logging

from typing import List, Tuple, Union, TYPE_CHECKING

if TYPE_CHECKING:
from git.repo import Repo

log = logging.getLogger('git.objects.commit')
log.addHandler(logging.NullHandler())

@@ -70,7 +75,8 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):

def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, author_tz_offset=None,
committer=None, committed_date=None, committer_tz_offset=None,
message=None, parents=None, encoding=None, gpgsig=None):
message=None, parents: Union[Tuple['Commit', ...], List['Commit'], None] = None,
encoding=None, gpgsig=None):
"""Instantiate a new Commit. All keyword arguments taking None as default will
be implicitly set on first query.

@@ -133,11 +139,11 @@ def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, aut
self.gpgsig = gpgsig

@classmethod
def _get_intermediate_items(cls, commit):
return commit.parents
def _get_intermediate_items(cls, commit: 'Commit') -> Tuple['Commit', ...]: # type: ignore ## cos overriding super
return tuple(commit.parents)

@classmethod
def _calculate_sha_(cls, repo, commit):
def _calculate_sha_(cls, repo: 'Repo', commit: 'Commit') -> bytes:
'''Calculate the sha of a commit.

:param repo: Repo object the commit should be part of
@@ -430,7 +436,7 @@ def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False,

#{ Serializable Implementation

def _serialize(self, stream):
def _serialize(self, stream: BytesIO) -> 'Commit':
write = stream.write
write(("tree %s\n" % self.tree).encode('ascii'))
for p in self.parents:
@@ -471,7 +477,7 @@ def _serialize(self, stream):
# END handle encoding
return self

def _deserialize(self, stream):
def _deserialize(self, stream: BytesIO) -> 'Commit':
""":param from_rev_list: if true, the stream format is coming from the rev-list command
Otherwise it is assumed to be a plain data stream from our object"""
readline = stream.readline
@@ -511,7 +517,7 @@ def _deserialize(self, stream):
buf = enc.strip()
while buf:
if buf[0:10] == b"encoding ":
self.encoding = buf[buf.find(' ') + 1:].decode(
self.encoding = buf[buf.find(b' ') + 1:].decode(
self.encoding, 'ignore')
elif buf[0:7] == b"gpgsig ":
sig = buf[buf.find(b' ') + 1:] + b"\n"
6 changes: 4 additions & 2 deletions git/objects/submodule/base.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import logging
import os
import stat
from typing import List
from unittest import SkipTest
import uuid

@@ -134,10 +135,11 @@ def _set_cache_(self, attr):
super(Submodule, self)._set_cache_(attr)
# END handle attribute name

def _get_intermediate_items(self, item):
@classmethod
def _get_intermediate_items(cls, item: 'Submodule') -> List['Submodule']: # type: ignore
""":return: all the submodules of our module repository"""
try:
return type(self).list_items(item.module())
return cls.list_items(item.module())
except InvalidGitRepositoryError:
return []
# END handle intermediate items
28 changes: 22 additions & 6 deletions git/objects/tag.py
Original file line number Diff line number Diff line change
@@ -9,6 +9,15 @@
from ..util import hex_to_bin
from ..compat import defenc

from typing import List, TYPE_CHECKING, Union

if TYPE_CHECKING:
from git.repo import Repo
from git.util import Actor
from .commit import Commit
from .blob import Blob
from .tree import Tree

__all__ = ("TagObject", )


@@ -18,8 +27,14 @@ class TagObject(base.Object):
type = "tag"
__slots__ = ("object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message")

def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment
tagger=None, tagged_date=None, tagger_tz_offset=None, message=None):
def __init__(self, repo: 'Repo', binsha: bytes,
object: Union[None, base.Object] = None,
tag: Union[None, str] = None,
tagger: Union[None, 'Actor'] = None,
tagged_date: Union[int, None] = None,
tagger_tz_offset: Union[int, None] = None,
message: Union[str, None] = None
) -> None: # @ReservedAssignment
"""Initialize a tag object with additional data

:param repo: repository this object is located in
@@ -34,7 +49,7 @@ def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment
authored_date is in, in a format similar to time.altzone"""
super(TagObject, self).__init__(repo, binsha)
if object is not None:
self.object = object
self.object = object # type: Union['Commit', 'Blob', 'Tree', 'TagObject']
if tag is not None:
self.tag = tag
if tagger is not None:
@@ -46,16 +61,17 @@ def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment
if message is not None:
self.message = message

def _set_cache_(self, attr):
def _set_cache_(self, attr: str) -> None:
"""Cache all our attributes at once"""
if attr in TagObject.__slots__:
ostream = self.repo.odb.stream(self.binsha)
lines = ostream.read().decode(defenc, 'replace').splitlines()
lines = ostream.read().decode(defenc, 'replace').splitlines() # type: List[str]

_obj, hexsha = lines[0].split(" ")
_type_token, type_name = lines[1].split(" ")
object_type = get_object_type_by_name(type_name.encode('ascii'))
self.object = \
get_object_type_by_name(type_name.encode('ascii'))(self.repo, hex_to_bin(hexsha))
object_type(self.repo, hex_to_bin(hexsha))

self.tag = lines[2][4:] # tag <tag name>

22 changes: 18 additions & 4 deletions git/objects/tree.py
Original file line number Diff line number Diff line change
@@ -17,6 +17,17 @@
tree_to_stream
)


# typing -------------------------------------------------

from typing import Iterable, Iterator, Tuple, Union, cast, TYPE_CHECKING

if TYPE_CHECKING:
from io import BytesIO

#--------------------------------------------------------


cmp = lambda a, b: (a > b) - (a < b)

__all__ = ("TreeModifier", "Tree")
@@ -182,8 +193,10 @@ def __init__(self, repo, binsha, mode=tree_id << 12, path=None):
super(Tree, self).__init__(repo, binsha, mode, path)

@classmethod
def _get_intermediate_items(cls, index_object):
def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore
) -> Tuple['Tree', ...]:
if index_object.type == "tree":
index_object = cast('Tree', index_object)
return tuple(index_object._iter_convert_to_object(index_object._cache))
return ()

@@ -196,7 +209,8 @@ def _set_cache_(self, attr):
super(Tree, self)._set_cache_(attr)
# END handle attribute

def _iter_convert_to_object(self, iterable):
def _iter_convert_to_object(self, iterable: Iterable[Tuple[bytes, int, str]]
) -> Iterator[Union[Blob, 'Tree', Submodule]]:
"""Iterable yields tuples of (binsha, mode, name), which will be converted
to the respective object representation"""
for binsha, mode, name in iterable:
@@ -317,15 +331,15 @@ def __contains__(self, item):
def __reversed__(self):
return reversed(self._iter_convert_to_object(self._cache))

def _serialize(self, stream):
def _serialize(self, stream: 'BytesIO') -> 'Tree':
"""Serialize this tree into the stream. Please note that we will assume
our tree data to be in a sorted state. If this is not the case, serialization
will not generate a correct tree representation as these are assumed to be sorted
by algorithms"""
tree_to_stream(self._cache, stream.write)
return self

def _deserialize(self, stream):
def _deserialize(self, stream: 'BytesIO') -> 'Tree':
self._cache = tree_entries_from_data(stream.read())
return self

129 changes: 91 additions & 38 deletions git/objects/util.py
Original file line number Diff line number Diff line change
@@ -4,19 +4,34 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
"""Module for general utility functions"""

from git.util import (
IterableList,
Actor
)

import re
from collections import deque as Deque
from collections import deque

from string import digits
import time
import calendar
from datetime import datetime, timedelta, tzinfo

# typing ------------------------------------------------------------
from typing import (Any, Callable, Deque, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload)

if TYPE_CHECKING:
from io import BytesIO, StringIO
from .submodule.base import Submodule
from .commit import Commit
from .blob import Blob
from .tag import TagObject
from .tree import Tree
from subprocess import Popen

# --------------------------------------------------------------------

__all__ = ('get_object_type_by_name', 'parse_date', 'parse_actor_and_date',
'ProcessStreamAdapter', 'Traversable', 'altz_to_utctz_str', 'utctz_to_altz',
'verify_utctz', 'Actor', 'tzoffset', 'utc')
@@ -26,7 +41,7 @@
#{ Functions


def mode_str_to_int(modestr):
def mode_str_to_int(modestr: Union[bytes, str]) -> int:
"""
:param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used
:return:
@@ -36,12 +51,14 @@ def mode_str_to_int(modestr):
for example."""
mode = 0
for iteration, char in enumerate(reversed(modestr[-6:])):
char = cast(Union[str, int], char)
mode += int(char) << iteration * 3
# END for each char
return mode


def get_object_type_by_name(object_type_name):
def get_object_type_by_name(object_type_name: bytes
) -> Union[Type['Commit'], Type['TagObject'], Type['Tree'], Type['Blob']]:
"""
:return: type suitable to handle the given object type name.
Use the type to create new instances.
@@ -62,18 +79,18 @@ def get_object_type_by_name(object_type_name):
from . import tree
return tree.Tree
else:
raise ValueError("Cannot handle unknown object type: %s" % object_type_name)
raise ValueError("Cannot handle unknown object type: %s" % object_type_name.decode())


def utctz_to_altz(utctz):
def utctz_to_altz(utctz: str) -> int:
"""we convert utctz to the timezone in seconds, it is the format time.altzone
returns. Git stores it as UTC timezone which has the opposite sign as well,
which explains the -1 * ( that was made explicit here )
:param utctz: git utc timezone string, i.e. +0200"""
return -1 * int(float(utctz) / 100 * 3600)


def altz_to_utctz_str(altz):
def altz_to_utctz_str(altz: int) -> str:
"""As above, but inverses the operation, returning a string that can be used
in commit objects"""
utci = -1 * int((float(altz) / 3600) * 100)
@@ -83,7 +100,7 @@ def altz_to_utctz_str(altz):
return prefix + utcs


def verify_utctz(offset):
def verify_utctz(offset: str) -> str:
""":raise ValueError: if offset is incorrect
:return: offset"""
fmt_exc = ValueError("Invalid timezone offset format: %s" % offset)
@@ -101,27 +118,28 @@ def verify_utctz(offset):


class tzoffset(tzinfo):
def __init__(self, secs_west_of_utc, name=None):

def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> None:
self._offset = timedelta(seconds=-secs_west_of_utc)
self._name = name or 'fixed'

def __reduce__(self):
def __reduce__(self) -> Tuple[Type['tzoffset'], Tuple[float, str]]:
return tzoffset, (-self._offset.total_seconds(), self._name)

def utcoffset(self, dt):
def utcoffset(self, dt) -> timedelta:
return self._offset

def tzname(self, dt):
def tzname(self, dt) -> str:
return self._name

def dst(self, dt):
def dst(self, dt) -> timedelta:
return ZERO


utc = tzoffset(0, 'UTC')


def from_timestamp(timestamp, tz_offset):
def from_timestamp(timestamp, tz_offset: float) -> datetime:
"""Converts a timestamp + tz_offset into an aware datetime instance."""
utc_dt = datetime.fromtimestamp(timestamp, utc)
try:
@@ -131,7 +149,7 @@ def from_timestamp(timestamp, tz_offset):
return utc_dt


def parse_date(string_date):
def parse_date(string_date: str) -> Tuple[int, int]:
"""
Parse the given date as one of the following
@@ -152,18 +170,18 @@ def parse_date(string_date):
# git time
try:
if string_date.count(' ') == 1 and string_date.rfind(':') == -1:
timestamp, offset = string_date.split()
timestamp, offset_str = string_date.split()
if timestamp.startswith('@'):
timestamp = timestamp[1:]
timestamp = int(timestamp)
return timestamp, utctz_to_altz(verify_utctz(offset))
timestamp_int = int(timestamp)
return timestamp_int, utctz_to_altz(verify_utctz(offset_str))
else:
offset = "+0000" # local time by default
offset_str = "+0000" # local time by default
if string_date[-5] in '-+':
offset = verify_utctz(string_date[-5:])
offset_str = verify_utctz(string_date[-5:])
string_date = string_date[:-6] # skip space as well
# END split timezone info
offset = utctz_to_altz(offset)
offset = utctz_to_altz(offset_str)

# now figure out the date and time portion - split time
date_formats = []
@@ -218,13 +236,13 @@ def parse_date(string_date):
_re_only_actor = re.compile(r'^.+? (.*)$')


def parse_actor_and_date(line):
def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
"""Parse out the actor (author or committer) info from a line like::
author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
:return: [Actor, int_seconds_since_epoch, int_timezone_offset]"""
actor, epoch, offset = '', 0, 0
actor, epoch, offset = '', '0', '0'
m = _re_actor_epoch.search(line)
if m:
actor, epoch, offset = m.groups()
@@ -247,11 +265,11 @@ class ProcessStreamAdapter(object):
it if the instance goes out of scope."""
__slots__ = ("_proc", "_stream")

def __init__(self, process, stream_name):
def __init__(self, process: 'Popen', stream_name: str) -> None:
self._proc = process
self._stream = getattr(process, stream_name)
self._stream = getattr(process, stream_name) # type: StringIO ## guess

def __getattr__(self, attr):
def __getattr__(self, attr: str) -> Any:
return getattr(self._stream, attr)


@@ -260,29 +278,61 @@ class Traversable(object):
"""Simple interface to perform depth-first or breadth-first traversals
into one direction.
Subclasses only need to implement one function.
Instances of the Subclass must be hashable"""
Instances of the Subclass must be hashable
Defined subclasses = [Commit, Tree, SubModule]
"""
__slots__ = ()

@overload
@classmethod
def _get_intermediate_items(cls, item: 'Commit') -> Tuple['Commit', ...]:
...

@overload
@classmethod
def _get_intermediate_items(cls, item: 'Submodule') -> Tuple['Submodule', ...]:
...

@overload
@classmethod
def _get_intermediate_items(cls, item: 'Tree') -> Tuple['Tree', ...]:
...

@overload
@classmethod
def _get_intermediate_items(cls, item: 'Traversable') -> Tuple['Traversable', ...]:
...

@classmethod
def _get_intermediate_items(cls, item):
def _get_intermediate_items(cls, item: 'Traversable'
) -> Sequence['Traversable']:
"""
Returns:
List of items connected to the given item.
Tuple of items connected to the given item.
Must be implemented in subclass
class Commit:: (cls, Commit) -> Tuple[Commit, ...]
class Submodule:: (cls, Submodule) -> Iterablelist[Submodule]
class Tree:: (cls, Tree) -> Tuple[Tree, ...]
"""
raise NotImplementedError("To be implemented in subclass")

def list_traverse(self, *args, **kwargs):
def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList:
"""
:return: IterableList with the results of the traversal as produced by
traverse()"""
out = IterableList(self._id_attribute_)
out = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses
out.extend(self.traverse(*args, **kwargs))
return out

def traverse(self, predicate=lambda i, d: True,
prune=lambda i, d: False, depth=-1, branch_first=True,
visit_once=True, ignore_self=1, as_edge=False):
def traverse(self,
predicate: Callable[[object, int], bool] = lambda i, d: True,
prune: Callable[[object, int], bool] = lambda i, d: False,
depth: int = -1,
branch_first: bool = True,
visit_once: bool = True, ignore_self: int = 1, as_edge: bool = False
) -> Union[Iterator['Traversable'], Iterator[Tuple['Traversable', 'Traversable']]]:
""":return: iterator yielding of items found when traversing self
:param predicate: f(i,d) returns False if item i at depth d should not be included in the result
@@ -314,13 +364,16 @@ def traverse(self, predicate=lambda i, d: True,
destination, i.e. tuple(src, dest) with the edge spanning from
source to destination"""
visited = set()
stack = Deque()
stack = deque() # type: Deque[Tuple[int, Traversable, Union[Traversable, None]]]
stack.append((0, self, None)) # self is always depth level 0

def addToStack(stack, item, branch_first, depth):
def addToStack(stack: Deque[Tuple[int, 'Traversable', Union['Traversable', None]]],
item: 'Traversable',
branch_first: bool,
depth) -> None:
lst = self._get_intermediate_items(item)
if not lst:
return
return None
if branch_first:
stack.extendleft((depth, i, item) for i in lst)
else:
@@ -359,14 +412,14 @@ class Serializable(object):
"""Defines methods to serialize and deserialize objects from and into a data stream"""
__slots__ = ()

def _serialize(self, stream):
def _serialize(self, stream: 'BytesIO') -> 'Serializable':
"""Serialize the data of this object into the given data stream
:note: a serialized object would ``_deserialize`` into the same object
:param stream: a file-like object
:return: self"""
raise NotImplementedError("To be implemented in subclass")

def _deserialize(self, stream):
def _deserialize(self, stream: 'BytesIO') -> 'Serializable':
"""Deserialize all information regarding this object from the stream
:param stream: a file-like object
:return: self"""
2 changes: 1 addition & 1 deletion git/repo/base.py
Original file line number Diff line number Diff line change
@@ -530,7 +530,7 @@ def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator['Tree']:
:note: Takes all arguments known to iter_commits method"""
return (c.tree for c in self.iter_commits(*args, **kwargs))

def tree(self, rev: Union['Commit', 'Tree', None] = None) -> 'Tree':
def tree(self, rev: Union['Commit', 'Tree', str, None] = None) -> 'Tree':
"""The Tree object for the given treeish revision
Examples::
9 changes: 3 additions & 6 deletions git/types.py
Original file line number Diff line number Diff line change
@@ -7,15 +7,12 @@
from typing import Union, Any

if sys.version_info[:2] >= (3, 8):
from typing import Final, Literal # noqa: F401
from typing import Final, Literal, SupportsIndex # noqa: F401
else:
from typing_extensions import Final, Literal # noqa: F401
from typing_extensions import Final, Literal, SupportsIndex # noqa: F401


if sys.version_info[:2] < (3, 6):
# os.PathLike (PEP-519) only got introduced with Python 3.6
PathLike = str
elif sys.version_info[:2] < (3, 9):
if sys.version_info[:2] < (3, 9):
# Python >= 3.6, < 3.9
PathLike = Union[str, os.PathLike]
elif sys.version_info[:2] >= (3, 9):
12 changes: 8 additions & 4 deletions git/util.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@
if TYPE_CHECKING:
from git.remote import Remote
from git.repo.base import Repo
from .types import PathLike, TBD, Literal
from .types import PathLike, TBD, Literal, SupportsIndex

# ---------------------------------------------------------------------

@@ -971,7 +971,10 @@ def __getattr__(self, attr: str) -> Any:
# END for each item
return list.__getattribute__(self, attr)

def __getitem__(self, index: Union[int, slice, str]) -> Any:
def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any:

assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str"

if isinstance(index, int):
return list.__getitem__(self, index)
elif isinstance(index, slice):
@@ -983,12 +986,13 @@ def __getitem__(self, index: Union[int, slice, str]) -> Any:
raise IndexError("No item found with id %r" % (self._prefix + index)) from e
# END handle getattr

def __delitem__(self, index: Union[int, str, slice]) -> None:
def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any:

assert isinstance(index, (int, str)), "Index of IterableList should be an int or str"

delindex = cast(int, index)
if not isinstance(index, int):
delindex = -1
assert not isinstance(index, slice)
name = self._prefix + index
for i, item in enumerate(self):
if getattr(item, self._id_attr) == name:
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -123,7 +123,6 @@ def build_py_modules(basedir, excludes=[]):
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",