diff --git a/.travis.yml b/.travis.yml index 570beaad6..1bed8368c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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" diff --git a/git/cmd.py b/git/cmd.py index 4f58b3146..e078e4a18 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -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 diff --git a/git/diff.py b/git/diff.py index a40fc244e..346a2ca7b 100644 --- a/git/diff.py +++ b/git/diff.py @@ -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})') diff --git a/git/index/fun.py b/git/index/fun.py index 1012f4801..3fded3473 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -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 diff --git a/git/objects/base.py b/git/objects/base.py index 59f0e8368..884f96515 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -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 '' % (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") diff --git a/git/objects/blob.py b/git/objects/blob.py index 897f892bf..017178f05 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -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 diff --git a/git/objects/commit.py b/git/objects/commit.py index 45e6d772c..26db6e36d 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -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" diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index e3be1a728..b03fa22a5 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -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 diff --git a/git/objects/tag.py b/git/objects/tag.py index b9bc6c248..cb6efbe9b 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -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 diff --git a/git/objects/tree.py b/git/objects/tree.py index 68e98329b..29b2a6846 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -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,7 +331,7 @@ 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 @@ -325,7 +339,7 @@ def _serialize(self, stream): 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 diff --git a/git/objects/util.py b/git/objects/util.py index d15d83c35..087f0166b 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -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,10 +79,10 @@ 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 ) @@ -73,7 +90,7 @@ def utctz_to_altz(utctz): 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 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""" diff --git a/git/repo/base.py b/git/repo/base.py index 6cc560310..5abd49618 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -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:: diff --git a/git/types.py b/git/types.py index 91d35b567..a410cb366 100644 --- a/git/types.py +++ b/git/types.py @@ -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): diff --git a/git/util.py b/git/util.py index edbd5f1e7..516c315c1 100644 --- a/git/util.py +++ b/git/util.py @@ -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: diff --git a/setup.py b/setup.py index 850d680d4..2845bbecd 100755 --- a/setup.py +++ b/setup.py @@ -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",