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
Show file tree
Hide file tree
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"
Expand Down
3 changes: 1 addition & 2 deletions git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}


Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions git/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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})')

Expand Down
2 changes: 1 addition & 1 deletion git/index/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 44 additions & 21 deletions git/objects/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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.

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"""
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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())

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions git/objects/submodule/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
import stat
from typing import List
from unittest import SkipTest
import uuid

Expand Down Expand Up @@ -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
Expand Down
28 changes: 22 additions & 6 deletions git/objects/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", )


Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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>

Expand Down
Loading