summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYobmod <yobmod@gmail.com>2021-06-23 02:22:34 +0100
committerYobmod <yobmod@gmail.com>2021-06-23 02:22:34 +0100
commit5b6fe83f4d817a3b73b44df16cfb4f96bd4d9904 (patch)
treee9030835ef3a199a650e9d948c03eea99a09696d
parent7ca97dcef3131a11dd5ef41d674bb6bd36608608 (diff)
downloadgitpython-5b6fe83f4d817a3b73b44df16cfb4f96bd4d9904.tar.gz
Update typing-extensions version in requirements.txt
-rw-r--r--.appveyor.yml29
-rw-r--r--.github/workflows/pythonpackage.yml2
-rw-r--r--.travis.yml44
-rw-r--r--CHANGES2
-rw-r--r--README.md2
-rw-r--r--VERSION2
-rw-r--r--doc/source/changes.rst8
-rw-r--r--doc/source/intro.rst2
-rw-r--r--doc/source/tutorial.rst2
-rw-r--r--git/cmd.py12
-rw-r--r--git/diff.py4
-rw-r--r--git/index/fun.py2
-rw-r--r--git/objects/base.py65
-rw-r--r--git/objects/blob.py4
-rw-r--r--git/objects/commit.py20
-rw-r--r--git/objects/submodule/base.py6
-rw-r--r--git/objects/tag.py28
-rw-r--r--git/objects/tree.py22
-rw-r--r--git/objects/util.py129
-rw-r--r--git/repo/base.py22
-rw-r--r--git/types.py9
-rw-r--r--git/util.py12
-rw-r--r--requirements.txt2
-rwxr-xr-xsetup.py5
-rw-r--r--test-requirements.txt2
-rw-r--r--test/test_repo.py30
-rw-r--r--tox.ini2
27 files changed, 279 insertions, 190 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index 0a86c1a7..833f5c7b 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -6,29 +6,12 @@ environment:
CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%"
matrix:
- - PYTHON: "C:\\Python34-x64"
- PYTHON_VERSION: "3.4"
- GIT_PATH: "%GIT_DAEMON_PATH%"
- - PYTHON: "C:\\Python35-x64"
- PYTHON_VERSION: "3.5"
- GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Python36-x64"
PYTHON_VERSION: "3.6"
GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Python37-x64"
PYTHON_VERSION: "3.7"
GIT_PATH: "%GIT_DAEMON_PATH%"
- - PYTHON: "C:\\Miniconda35-x64"
- PYTHON_VERSION: "3.5"
- IS_CONDA: "yes"
- MAYFAIL: "yes"
- GIT_PATH: "%GIT_DAEMON_PATH%"
- ## Cygwin
- - PYTHON: "C:\\Python35-x64"
- PYTHON_VERSION: "3.5"
- IS_CYGWIN: "yes"
- MAYFAIL: "yes"
- GIT_PATH: "%CYGWIN64_GIT_PATH%"
matrix:
allow_failures:
@@ -76,18 +59,10 @@ install:
build: false
test_script:
- - IF "%IS_CYGWIN%" == "yes" (
- nosetests -v
- ) ELSE (
- IF "%PYTHON_VERSION%" == "3.5" (
- nosetests -v --with-coverage
- ) ELSE (
- nosetests -v
- )
- )
+ - nosetests -v
on_success:
- - IF "%PYTHON_VERSION%" == "3.5" IF NOT "%IS_CYGWIN%" == "yes" (codecov)
+ - IF "%PYTHON_VERSION%" == "3.6" IF NOT "%IS_CYGWIN%" == "yes" (codecov)
# Enable this to be able to login to the build worker. You can use the
# `remmina` program in Ubuntu, use the login information that the line below
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 3c7215cb..53da7614 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
+ python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1fbb1ddb..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-# UNUSED, only for reference. If adjustments are needed, please see github actions
-language: python
-python:
- - "3.4"
- - "3.5"
- - "3.6"
- - "3.7"
- - "3.8"
- - "nightly"
- # - "pypy" - won't work as smmap doesn't work (see gitdb/.travis.yml for details)
-matrix:
- allow_failures:
- - python: "nightly"
-git:
- # a higher depth is needed for most of the tests - must be high enough to not actually be shallow
- # as we clone our own repository in the process
- depth: 99999
-install:
- - python --version; git --version
- - git submodule update --init --recursive
- - git fetch --tags
- - pip install -r test-requirements.txt
- - pip install -r doc/requirements.txt
- - pip install codecov
-
- # generate some reflog as git-python tests need it (in master)
- - ./init-tests-after-clone.sh
-
- # as commits are performed with the default user, it needs to be set for travis too
- - git config --global user.email "travis@ci.com"
- - git config --global user.name "Travis Runner"
- # If we rewrite the user's config by accident, we will mess it up
- # and cause subsequent tests to fail
- - cat git/test/fixtures/.gitconfig >> ~/.gitconfig
-script:
- # Make sure we limit open handles to see if we are leaking them
- - ulimit -n 128
- - ulimit -n
- - coverage run --omit="test/*" -m unittest --buffer
- - coverage report
- - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi
- - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi
-after_success:
- - codecov
diff --git a/CHANGES b/CHANGES
index aa8116b2..9796566a 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,2 +1,2 @@
Please see the online documentation for the latest changelog:
-https://github.com/gitpython-developers/GitPython/blob/master/doc/source/changes.rst
+https://github.com/gitpython-developers/GitPython/blob/main/doc/source/changes.rst
diff --git a/README.md b/README.md
index 0d0edeb4..4725d3ae 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting
the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
* Git (1.7.x or newer)
-* Python >= 3.5
+* Python >= 3.6
The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
The installer takes care of installing them for you.
diff --git a/VERSION b/VERSION
index 3797f3f9..5762a6ff 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.17
+3.1.18
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 68a94516..aabef802 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,14 @@
Changelog
=========
+3.1.18
+======
+
+* drop support for python 3.5 to reduce maintenance burden on typing. Lower patch levels of python 3.5 would break, too.
+
+See the following for details:
+https://github.com/gitpython-developers/gitpython/milestone/50?closed=1
+
3.1.17
======
diff --git a/doc/source/intro.rst b/doc/source/intro.rst
index 7168c91b..956a3607 100644
--- a/doc/source/intro.rst
+++ b/doc/source/intro.rst
@@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of
Requirements
============
-* `Python`_ >= 3.5
+* `Python`_ >= 3.6
* `Git`_ 1.7.0 or newer
It should also work with older versions, but it may be that some operations
involving remotes will not work as expected.
diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst
index d548f882..303e89cf 100644
--- a/doc/source/tutorial.rst
+++ b/doc/source/tutorial.rst
@@ -10,7 +10,7 @@ GitPython Tutorial
GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explains a real-life usecase.
-All code presented here originated from `test_docs.py <https://github.com/gitpython-developers/GitPython/blob/master/test/test_docs.py>`_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python.
+All code presented here originated from `test_docs.py <https://github.com/gitpython-developers/GitPython/blob/main/test/test_docs.py>`_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python.
Meet the Repo type
******************
diff --git a/git/cmd.py b/git/cmd.py
index d8b82352..e078e4a1 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -17,9 +17,7 @@ from subprocess import (
import subprocess
import sys
import threading
-from collections import OrderedDict
from textwrap import dedent
-import warnings
from git.compat import (
defenc,
@@ -150,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}
@@ -462,7 +459,7 @@ class Git(LazyMixin):
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
@@ -1005,13 +1002,6 @@ class Git(LazyMixin):
def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
"""Transforms Python style kwargs into git command line options."""
- # Python 3.6 preserves the order of kwargs and thus has a stable
- # order. For older versions sort the kwargs by the key to get a stable
- # order.
- if sys.version_info[:2] < (3, 6):
- kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0]))
- warnings.warn("Python 3.5 support is deprecated and will be removed 2021-09-05.\n" +
- "It does not preserve the order for key-word arguments and enforce lexical sorting instead.")
args = []
for k, v in kwargs.items():
if isinstance(v, (list, tuple)):
diff --git a/git/diff.py b/git/diff.py
index a40fc244..346a2ca7 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -16,7 +16,7 @@ from .objects.util import mode_str_to_int
# 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 @@ Lit_change_type = Literal['A', 'D', 'M', 'R', 'T']
__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 1012f480..3fded347 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 59f0e836..884f9651 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 @@ class Object(LazyMixin):
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 @@ class Object(LazyMixin):
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 @@ class Object(LazyMixin):
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 @@ class IndexObject(Object):
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 @@ class IndexObject(Object):
# 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 897f892b..017178f0 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 45e6d772..26db6e36 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -36,6 +36,11 @@ import os
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 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
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 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
#{ 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 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
# 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 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
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 e3be1a72..b03fa22a 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -3,6 +3,7 @@ from io import BytesIO
import logging
import os
import stat
+from typing import List
from unittest import SkipTest
import uuid
@@ -134,10 +135,11 @@ class Submodule(IndexObject, Iterable, Traversable):
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 b9bc6c24..cb6efbe9 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -9,6 +9,15 @@ from .util import get_object_type_by_name, parse_actor_and_date
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 @@ class TagObject(base.Object):
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 @@ class TagObject(base.Object):
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>
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 68e98329..29b2a684 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -17,6 +17,17 @@ from .fun import (
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 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable):
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 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable):
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 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable):
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 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable):
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 d15d83c3..087f0166 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 @@ ZERO = timedelta(0)
#{ 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 @@ _re_actor_epoch = re.compile(r'^.+? (.*) (\d+) ([+-]\d+).*$')
_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 @@ class Traversable(object):
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 55682411..5abd4961 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -3,12 +3,13 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
-
import logging
import os
import re
import warnings
+from gitdb.exc import BadObject
+
from git.cmd import (
Git,
handle_process_output
@@ -529,7 +530,7 @@ class Repo(object):
: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::
@@ -618,6 +619,23 @@ class Repo(object):
raise
return True
+ def is_valid_object(self, sha: str, object_type: str = None) -> bool:
+ try:
+ complete_sha = self.odb.partial_to_complete_sha_hex(sha)
+ object_info = self.odb.info(complete_sha)
+ if object_type:
+ if object_info.type == object_type.encode():
+ return True
+ else:
+ log.debug("Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
+ object_info.type.decode(), object_type)
+ return False
+ else:
+ return True
+ except BadObject:
+ log.debug("Commit hash is invalid.")
+ return False
+
def _get_daemon_export(self) -> bool:
if self.git_dir:
filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
diff --git a/git/types.py b/git/types.py
index 91d35b56..a410cb36 100644
--- a/git/types.py
+++ b/git/types.py
@@ -7,15 +7,12 @@ import sys
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 edbd5f1e..516c315c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -29,7 +29,7 @@ import pathlib
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 @@ class IterableList(list):
# 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 @@ class IterableList(list):
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/requirements.txt b/requirements.txt
index d980f668..7159416a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
gitdb>=4.0.1,<5
-typing-extensions>=3.7.4.0;python_version<"3.8"
+typing-extensions>=3.7.4.3;python_version<"3.8"
diff --git a/setup.py b/setup.py
index f8829c38..2845bbec 100755
--- a/setup.py
+++ b/setup.py
@@ -99,7 +99,7 @@ setup(
include_package_data=True,
py_modules=build_py_modules("./git", excludes=["git.ext.*"]),
package_dir={'git': 'git'},
- python_requires='>=3.5',
+ python_requires='>=3.6',
install_requires=requirements,
tests_require=requirements + test_requirements,
zip_safe=False,
@@ -123,10 +123,9 @@ setup(
"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",
- "Programming Language :: Python :: 3.9"
+ "Programming Language :: Python :: 3.9"
]
)
diff --git a/test-requirements.txt b/test-requirements.txt
index e06d2be1..16dc0d2c 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,4 +5,4 @@ tox
virtualenv
nose
gitdb>=4.0.1,<5
-typing-extensions>=3.7.4.0;python_version<"3.8"
+typing-extensions>=3.7.4.3;python_version<"3.8"
diff --git a/test/test_repo.py b/test/test_repo.py
index 0311653a..8aced94d 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -422,7 +422,7 @@ class TestRepo(TestBase):
self.rorepo.tag(tag)
except ValueError as valueError:
value_errors.append(valueError.args[0])
- raise ValueError('. '.join(value_errors))
+ self.assertEqual(value_errors, [])
def test_archive(self):
tmpfile = tempfile.mktemp(suffix='archive-test')
@@ -989,6 +989,34 @@ class TestRepo(TestBase):
for i, j in itertools.permutations([c1, 'ffffff', ''], r=2):
self.assertRaises(GitCommandError, repo.is_ancestor, i, j)
+ def test_is_valid_object(self):
+ repo = self.rorepo
+ commit_sha = 'f6aa8d1'
+ blob_sha = '1fbe3e4375'
+ tree_sha = '960b40fe36'
+ tag_sha = '42c2f60c43'
+
+ # Check for valid objects
+ self.assertTrue(repo.is_valid_object(commit_sha))
+ self.assertTrue(repo.is_valid_object(blob_sha))
+ self.assertTrue(repo.is_valid_object(tree_sha))
+ self.assertTrue(repo.is_valid_object(tag_sha))
+
+ # Check for valid objects of specific type
+ self.assertTrue(repo.is_valid_object(commit_sha, 'commit'))
+ self.assertTrue(repo.is_valid_object(blob_sha, 'blob'))
+ self.assertTrue(repo.is_valid_object(tree_sha, 'tree'))
+ self.assertTrue(repo.is_valid_object(tag_sha, 'tag'))
+
+ # Check for invalid objects
+ self.assertFalse(repo.is_valid_object(b'1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a', 'blob'))
+
+ # Check for invalid objects of specific type
+ self.assertFalse(repo.is_valid_object(commit_sha, 'blob'))
+ self.assertFalse(repo.is_valid_object(blob_sha, 'commit'))
+ self.assertFalse(repo.is_valid_object(tree_sha, 'commit'))
+ self.assertFalse(repo.is_valid_object(tag_sha, 'commit'))
+
@with_rw_directory
def test_git_work_tree_dotgit(self, rw_dir):
"""Check that we find .git as a worktree file and find the worktree
diff --git a/tox.ini b/tox.ini
index a0cb1c9f..e3dd84b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py35,py36,py37,py38,py39,flake8
+envlist = py36,py37,py38,py39,flake8
[testenv]
commands = python -m unittest --buffer {posargs}