From 5b6fe83f4d817a3b73b44df16cfb4f96bd4d9904 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Wed, 23 Jun 2021 02:22:34 +0100 Subject: [PATCH 01/26] Update typing-extensions version in requirements.txt --- .appveyor.yml | 29 +------ .github/workflows/pythonpackage.yml | 2 +- .travis.yml | 44 ---------- CHANGES | 2 +- README.md | 2 +- VERSION | 2 +- doc/source/changes.rst | 8 ++ doc/source/intro.rst | 2 +- doc/source/tutorial.rst | 2 +- git/cmd.py | 12 +-- git/diff.py | 4 +- git/index/fun.py | 2 +- git/objects/base.py | 65 +++++++++----- git/objects/blob.py | 4 +- git/objects/commit.py | 20 +++-- git/objects/submodule/base.py | 6 +- git/objects/tag.py | 28 ++++-- git/objects/tree.py | 22 ++++- git/objects/util.py | 129 ++++++++++++++++++++-------- git/repo/base.py | 22 ++++- git/types.py | 9 +- git/util.py | 12 ++- requirements.txt | 2 +- setup.py | 5 +- test-requirements.txt | 2 +- test/test_repo.py | 30 ++++++- tox.ini | 2 +- 27 files changed, 279 insertions(+), 190 deletions(-) delete mode 100644 .travis.yml diff --git a/.appveyor.yml b/.appveyor.yml index 0a86c1a75..833f5c7b9 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 3c7215cbe..53da76149 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 1fbb1ddb8..000000000 --- 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 aa8116b23..9796566ae 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 0d0edeb43..4725d3aeb 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=` 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 3797f3f9c..5762a6ffe 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 68a94516c..aabef8023 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 7168c91b1..956a36073 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 d548f8829..303e89cff 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 `_ 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 `_ 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 d8b82352d..e078e4a18 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -17,9 +17,7 @@ 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 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 @@ -1005,13 +1002,6 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool 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 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 55682411a..5abd49618 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 @@ 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:: @@ -618,6 +619,23 @@ def is_ancestor(self, ancestor_rev: 'Commit', rev: 'Commit') -> bool: 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 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/requirements.txt b/requirements.txt index d980f6682..7159416a9 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 f8829c386..2845bbecd 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def build_py_modules(basedir, excludes=[]): 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 @@ 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", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9" ] ) diff --git a/test-requirements.txt b/test-requirements.txt index e06d2be14..16dc0d2c1 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 0311653a2..8aced94d4 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -422,7 +422,7 @@ def test_tag_to_full_tag_path(self): 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 @@ def test_is_ancestor(self): 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 a0cb1c9f1..e3dd84b6b 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} From 42e4f5e26b812385df65f8f32081035e2fb2a121 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 05:52:48 +0100 Subject: [PATCH 02/26] Add types to tree.Tree --- git/index/base.py | 2 +- git/index/fun.py | 2 +- git/objects/fun.py | 15 +++++++++--- git/objects/tree.py | 56 ++++++++++++++++++++++----------------------- git/repo/base.py | 3 ++- 5 files changed, 44 insertions(+), 34 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 044240602..e2b3f8fa4 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -568,7 +568,7 @@ def write_tree(self) -> Tree: # note: additional deserialization could be saved if write_tree_from_cache # would return sorted tree entries root_tree = Tree(self.repo, binsha, path='') - root_tree._cache = tree_items + root_tree._cache = tree_items # type: ignore return root_tree def _process_diff_args(self, args: List[Union[str, diff.Diffable, object]] diff --git a/git/index/fun.py b/git/index/fun.py index 3fded3473..10a440501 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -293,7 +293,7 @@ def write_tree_from_cache(entries: List[IndexEntry], odb, sl: slice, si: int = 0 # finally create the tree sio = BytesIO() tree_to_stream(tree_items, sio.write) # converts bytes of each item[0] to str - tree_items_stringified = cast(List[Tuple[str, int, str]], tree_items) # type: List[Tuple[str, int, str]] + tree_items_stringified = cast(List[Tuple[str, int, str]], tree_items) sio.seek(0) istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio)) diff --git a/git/objects/fun.py b/git/objects/fun.py index 9b36712e1..339a53b8c 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -1,10 +1,19 @@ """Module with functions which are supposed to be as fast as possible""" from stat import S_ISDIR + from git.compat import ( safe_decode, defenc ) +# typing ---------------------------------------------- + +from typing import List, Tuple + + +# --------------------------------------------------- + + __all__ = ('tree_to_stream', 'tree_entries_from_data', 'traverse_trees_recursive', 'traverse_tree_recursive') @@ -38,7 +47,7 @@ def tree_to_stream(entries, write): # END for each item -def tree_entries_from_data(data): +def tree_entries_from_data(data: bytes) -> List[Tuple[bytes, int, str]]: """Reads the binary representation of a tree and returns tuples of Tree items :param data: data block with tree data (as bytes) :return: list(tuple(binsha, mode, tree_relative_path), ...)""" @@ -72,8 +81,8 @@ def tree_entries_from_data(data): # default encoding for strings in git is utf8 # Only use the respective unicode object if the byte stream was encoded - name = data[ns:i] - name = safe_decode(name) + name_bytes = data[ns:i] + name = safe_decode(name_bytes) # byte is NULL, get next 20 i += 1 diff --git a/git/objects/tree.py b/git/objects/tree.py index 29b2a6846..ec7d8e885 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -20,20 +20,23 @@ # typing ------------------------------------------------- -from typing import Iterable, Iterator, Tuple, Union, cast, TYPE_CHECKING +from typing import Callable, Dict, Iterable, Iterator, List, Tuple, Type, Union, cast, TYPE_CHECKING + +from git.types import PathLike if TYPE_CHECKING: + from git.repo import Repo from io import BytesIO #-------------------------------------------------------- -cmp = lambda a, b: (a > b) - (a < b) +cmp: Callable[[int, int], int] = lambda a, b: (a > b) - (a < b) __all__ = ("TreeModifier", "Tree") -def git_cmp(t1, t2): +def git_cmp(t1: 'Tree', t2: 'Tree') -> int: a, b = t1[2], t2[2] len_a, len_b = len(a), len(b) min_len = min(len_a, len_b) @@ -45,9 +48,9 @@ def git_cmp(t1, t2): return len_a - len_b -def merge_sort(a, cmp): +def merge_sort(a: List[int], cmp: Callable[[int, int], int]) -> None: if len(a) < 2: - return + return None mid = len(a) // 2 lefthalf = a[:mid] @@ -182,29 +185,29 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable): symlink_id = 0o12 tree_id = 0o04 - _map_id_to_type = { + _map_id_to_type: Dict[int, Union[Type[Submodule], Type[Blob], Type['Tree']]] = { commit_id: Submodule, blob_id: Blob, symlink_id: Blob # tree id added once Tree is defined } - def __init__(self, repo, binsha, mode=tree_id << 12, path=None): + def __init__(self, repo: 'Repo', binsha: bytes, mode: int = tree_id << 12, path: Union[PathLike, None] = None): super(Tree, self).__init__(repo, binsha, mode, path) @classmethod def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore - ) -> Tuple['Tree', ...]: + ) -> Union[Tuple['Tree', ...], Tuple[()]]: if index_object.type == "tree": index_object = cast('Tree', index_object) return tuple(index_object._iter_convert_to_object(index_object._cache)) return () - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr == "_cache": # Set the data when we need it ostream = self.repo.odb.stream(self.binsha) - self._cache = tree_entries_from_data(ostream.read()) + self._cache: List[Tuple[bytes, int, str]] = tree_entries_from_data(ostream.read()) else: super(Tree, self)._set_cache_(attr) # END handle attribute @@ -221,7 +224,7 @@ def _iter_convert_to_object(self, iterable: Iterable[Tuple[bytes, int, str]] raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) from e # END for each item - def join(self, file): + def join(self, file: str) -> Union[Blob, 'Tree', Submodule]: """Find the named object in this tree's contents :return: ``git.Blob`` or ``git.Tree`` or ``git.Submodule`` @@ -254,26 +257,22 @@ def join(self, file): raise KeyError(msg % file) # END handle long paths - def __div__(self, file): - """For PY2 only""" - return self.join(file) - - def __truediv__(self, file): + def __truediv__(self, file: str) -> Union['Tree', Blob, Submodule]: """For PY3 only""" return self.join(file) @property - def trees(self): + def trees(self) -> List['Tree']: """:return: list(Tree, ...) list of trees directly below this tree""" return [i for i in self if i.type == "tree"] @property - def blobs(self): + def blobs(self) -> List['Blob']: """:return: list(Blob, ...) list of blobs directly below this tree""" return [i for i in self if i.type == "blob"] @property - def cache(self): + def cache(self) -> TreeModifier: """ :return: An object allowing to modify the internal cache. This can be used to change the tree's contents. When done, make sure you call ``set_done`` @@ -289,16 +288,16 @@ def traverse(self, predicate=lambda i, d: True, return super(Tree, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self) # List protocol - def __getslice__(self, i, j): + def __getslice__(self, i: int, j: int) -> List[Union[Blob, 'Tree', Submodule]]: return list(self._iter_convert_to_object(self._cache[i:j])) - def __iter__(self): + def __iter__(self) -> Iterator[Union[Blob, 'Tree', Submodule]]: return self._iter_convert_to_object(self._cache) - def __len__(self): + def __len__(self) -> int: return len(self._cache) - def __getitem__(self, item): + def __getitem__(self, item: Union[str, int, slice]) -> Union[Blob, 'Tree', Submodule]: if isinstance(item, int): info = self._cache[item] return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2])) @@ -310,7 +309,7 @@ def __getitem__(self, item): raise TypeError("Invalid index type: %r" % item) - def __contains__(self, item): + def __contains__(self, item: Union[IndexObject, PathLike]) -> bool: if isinstance(item, IndexObject): for info in self._cache: if item.binsha == info[0]: @@ -321,10 +320,11 @@ def __contains__(self, item): # compatibility # treat item as repo-relative path - path = self.path - for info in self._cache: - if item == join_path(path, info[2]): - return True + else: + path = self.path + for info in self._cache: + if item == join_path(path, info[2]): + return True # END for each item return False diff --git a/git/repo/base.py b/git/repo/base.py index 5abd49618..779477310 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -7,6 +7,7 @@ import os import re import warnings +from gitdb.db.loose import LooseObjectDB from gitdb.exc import BadObject @@ -100,7 +101,7 @@ class Repo(object): # Subclasses may easily bring in their own custom types by placing a constructor or type here GitCommandWrapperType = Git - def __init__(self, path: Optional[PathLike] = None, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, + def __init__(self, path: Optional[PathLike] = None, odbt: Type[LooseObjectDB] = GitCmdObjectDB, search_parent_directories: bool = False, expand_vars: bool = True) -> None: """Create a new Repo instance From c3903d8e03af5c1e01c1a96919b926c55f45052e Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 14:27:13 +0100 Subject: [PATCH 03/26] Make IterableList generic and update throughout --- git/objects/commit.py | 4 ++-- git/objects/submodule/base.py | 20 ++++++++++++++------ git/objects/util.py | 11 ++++++----- git/refs/reference.py | 4 ++-- git/remote.py | 31 ++++++++++++++++--------------- git/repo/base.py | 12 ++++++------ git/util.py | 22 ++++++++++++++++------ 7 files changed, 62 insertions(+), 42 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 26db6e36d..0b707450c 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -8,7 +8,7 @@ from git.util import ( hex_to_bin, Actor, - Iterable, + IterableObj, Stats, finalize_process ) @@ -47,7 +47,7 @@ __all__ = ('Commit', ) -class Commit(base.Object, Iterable, Diffable, Traversable, Serializable): +class Commit(base.Object, IterableObj, Diffable, Traversable, Serializable): """Wraps a git Commit object. diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 8cf4dd1eb..57396a467 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,7 +3,6 @@ import logging import os import stat -from typing import List from unittest import SkipTest import uuid @@ -27,7 +26,7 @@ from git.objects.base import IndexObject, Object from git.objects.util import Traversable from git.util import ( - Iterable, + IterableObj, join_path_native, to_native_path_linux, RemoteProgress, @@ -47,6 +46,15 @@ ) +# typing ---------------------------------------------------------------------- + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from git.util import IterableList + +# ----------------------------------------------------------------------------- + __all__ = ["Submodule", "UpdateProgress"] @@ -74,7 +82,7 @@ class UpdateProgress(RemoteProgress): # IndexObject comes via util module, its a 'hacky' fix thanks to pythons import # mechanism which cause plenty of trouble of the only reason for packages and # modules is refactoring - subpackages shouldn't depend on parent packages -class Submodule(IndexObject, Iterable, Traversable): +class Submodule(IndexObject, IterableObj, Traversable): """Implements access to a git submodule. They are special in that their sha represents a commit in the submodule's repository which is to be checked out @@ -136,12 +144,12 @@ def _set_cache_(self, attr): # END handle attribute name @classmethod - def _get_intermediate_items(cls, item: 'Submodule') -> List['Submodule']: # type: ignore + def _get_intermediate_items(cls, item: 'Submodule') -> IterableList['Submodule']: """:return: all the submodules of our module repository""" try: return cls.list_items(item.module()) except InvalidGitRepositoryError: - return [] + return IterableList('') # END handle intermediate items @classmethod @@ -1163,7 +1171,7 @@ def config_reader(self): :raise IOError: If the .gitmodules file/blob could not be read""" return self._config_parser_constrained(read_only=True) - def children(self): + def children(self) -> IterableList['Submodule']: """ :return: IterableList(Submodule, ...) an iterable list of submodules instances which are children of this submodule or 0 if the submodule is not checked out""" diff --git a/git/objects/util.py b/git/objects/util.py index 087f0166b..a565cf42f 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,11 +19,11 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload) +from typing import (Any, Callable, Deque, Iterator, TYPE_CHECKING, Tuple, Type, Union, cast) if TYPE_CHECKING: from io import BytesIO, StringIO - from .submodule.base import Submodule + from .submodule.base import Submodule # noqa: F401 from .commit import Commit from .blob import Blob from .tag import TagObject @@ -284,6 +284,7 @@ class Traversable(object): """ __slots__ = () + """ @overload @classmethod def _get_intermediate_items(cls, item: 'Commit') -> Tuple['Commit', ...]: @@ -303,10 +304,10 @@ def _get_intermediate_items(cls, item: 'Tree') -> Tuple['Tree', ...]: @classmethod def _get_intermediate_items(cls, item: 'Traversable') -> Tuple['Traversable', ...]: ... + """ @classmethod - def _get_intermediate_items(cls, item: 'Traversable' - ) -> Sequence['Traversable']: + def _get_intermediate_items(cls, item): """ Returns: Tuple of items connected to the given item. @@ -322,7 +323,7 @@ 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_) # type: ignore[attr-defined] # defined in sublcasses + out: IterableList = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses out.extend(self.traverse(*args, **kwargs)) return out diff --git a/git/refs/reference.py b/git/refs/reference.py index 9014f5558..8a9b04873 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -1,6 +1,6 @@ from git.util import ( LazyMixin, - Iterable, + IterableObj, ) from .symbolic import SymbolicReference @@ -23,7 +23,7 @@ def wrapper(self, *args): #}END utilities -class Reference(SymbolicReference, LazyMixin, Iterable): +class Reference(SymbolicReference, LazyMixin, IterableObj): """Represents a named reference to any object. Subclasses may apply restrictions though, i.e. Heads can only point to commits.""" diff --git a/git/remote.py b/git/remote.py index 6ea4b2a1a..a85297c17 100644 --- a/git/remote.py +++ b/git/remote.py @@ -13,7 +13,7 @@ from git.exc import GitCommandError from git.util import ( LazyMixin, - Iterable, + IterableObj, IterableList, RemoteProgress, CallableRemoteProgress, @@ -107,7 +107,7 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non return progress -class PushInfo(object): +class PushInfo(IterableObj, object): """ Carries information about the result of a push operation of a single head:: @@ -220,7 +220,7 @@ def _from_line(cls, remote: 'Remote', line: str) -> 'PushInfo': return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) -class FetchInfo(object): +class FetchInfo(IterableObj, object): """ Carries information about the results of a fetch operation of a single head:: @@ -421,7 +421,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': return cls(remote_local_ref, flags, note, old_commit, local_remote_ref) -class Remote(LazyMixin, Iterable): +class Remote(LazyMixin, IterableObj): """Provides easy read and write access to a git remote. @@ -580,18 +580,18 @@ def urls(self) -> Iterator[str]: raise ex @property - def refs(self) -> IterableList: + def refs(self) -> IterableList[RemoteReference]: """ :return: IterableList of RemoteReference objects. It is prefixed, allowing you to omit the remote path portion, i.e.:: remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')""" - out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name)) return out_refs @property - def stale_refs(self) -> IterableList: + def stale_refs(self) -> IterableList[Reference]: """ :return: IterableList RemoteReference objects that do not have a corresponding @@ -606,7 +606,7 @@ def stale_refs(self) -> IterableList: as well. This is a fix for the issue described here: https://github.com/gitpython-developers/GitPython/issues/260 """ - out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: # expecting # * [would prune] origin/new_branch @@ -681,11 +681,12 @@ def update(self, **kwargs: Any) -> 'Remote': return self def _get_fetch_info_from_stderr(self, proc: TBD, - progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList: + progress: Union[Callable[..., Any], RemoteProgress, None] + ) -> IterableList['FetchInfo']: progress = to_progress_instance(progress) # skip first line as it is some remote info we are not interested in - output = IterableList('name') + output: IterableList['FetchInfo'] = IterableList('name') # lines which are no progress are fetch info lines # this also waits for the command to finish @@ -741,7 +742,7 @@ def _get_fetch_info_from_stderr(self, proc: TBD, return output def _get_push_info(self, proc: TBD, - progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList: + progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList[PushInfo]: progress = to_progress_instance(progress) # read progress information from stderr @@ -749,7 +750,7 @@ def _get_push_info(self, proc: TBD, # read the lines manually as it will use carriage returns between the messages # to override the previous one. This is why we read the bytes manually progress_handler = progress.new_message_handler() - output = IterableList('push_infos') + output: IterableList[PushInfo] = IterableList('push_infos') def stdout_handler(line: str) -> None: try: @@ -785,7 +786,7 @@ def _assert_refspec(self) -> None: def fetch(self, refspec: Union[str, List[str], None] = None, progress: Union[Callable[..., Any], None] = None, - verbose: bool = True, **kwargs: Any) -> IterableList: + verbose: bool = True, **kwargs: Any) -> IterableList[FetchInfo]: """Fetch the latest changes for this remote :param refspec: @@ -832,7 +833,7 @@ def fetch(self, refspec: Union[str, List[str], None] = None, def pull(self, refspec: Union[str, List[str], None] = None, progress: Union[Callable[..., Any], None] = None, - **kwargs: Any) -> IterableList: + **kwargs: Any) -> IterableList[FetchInfo]: """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. @@ -853,7 +854,7 @@ def pull(self, refspec: Union[str, List[str], None] = None, def push(self, refspec: Union[str, List[str], None] = None, progress: Union[Callable[..., Any], None] = None, - **kwargs: Any) -> IterableList: + **kwargs: Any) -> IterableList[PushInfo]: """Push changes from source branch in refspec to target branch in refspec. :param refspec: see 'fetch' method diff --git a/git/repo/base.py b/git/repo/base.py index 779477310..52727504b 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -309,7 +309,7 @@ def bare(self) -> bool: return self._bare @property - def heads(self) -> 'IterableList': + def heads(self) -> 'IterableList[Head]': """A list of ``Head`` objects representing the branch heads in this repo @@ -317,7 +317,7 @@ def heads(self) -> 'IterableList': return Head.list_items(self) @property - def references(self) -> 'IterableList': + def references(self) -> 'IterableList[Reference]': """A list of Reference objects representing tags, heads and remote references. :return: IterableList(Reference, ...)""" @@ -342,7 +342,7 @@ def head(self) -> 'HEAD': return HEAD(self, 'HEAD') @property - def remotes(self) -> 'IterableList': + def remotes(self) -> 'IterableList[Remote]': """A list of Remote objects allowing to access and manipulate remotes :return: ``git.IterableList(Remote, ...)``""" return Remote.list_items(self) @@ -358,13 +358,13 @@ def remote(self, name: str = 'origin') -> 'Remote': #{ Submodules @property - def submodules(self) -> 'IterableList': + def submodules(self) -> 'IterableList[Submodule]': """ :return: git.IterableList(Submodule, ...) of direct submodules available from the current head""" return Submodule.list_items(self) - def submodule(self, name: str) -> 'IterableList': + def submodule(self, name: str) -> 'Submodule': """ :return: Submodule with the given name :raise ValueError: If no such submodule exists""" try: @@ -396,7 +396,7 @@ def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator: #}END submodules @property - def tags(self) -> 'IterableList': + def tags(self) -> 'IterableList[TagReference]': """A list of ``Tag`` objects that are available in this repo :return: ``git.IterableList(TagReference, ...)`` """ return TagReference.list_items(self) diff --git a/git/util.py b/git/util.py index 516c315c1..5f184b7a2 100644 --- a/git/util.py +++ b/git/util.py @@ -22,7 +22,7 @@ # typing --------------------------------------------------------- from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterator, List, - Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING, overload) + Optional, Pattern, Sequence, Tuple, TypeVar, Union, cast, TYPE_CHECKING, overload) import pathlib @@ -920,7 +920,10 @@ def _obtain_lock(self) -> None: # END endless loop -class IterableList(list): +T = TypeVar('T', bound='IterableObj') + + +class IterableList(List[T]): """ List of iterable objects allowing to query an object by id or by named index:: @@ -930,6 +933,9 @@ class IterableList(list): heads['master'] heads[0] + Iterable parent objects = [Commit, SubModule, Reference, FetchInfo, PushInfo] + Iterable via inheritance = [Head, TagReference, RemoteReference] + ] It requires an id_attribute name to be set which will be queried from its contained items to have a means for comparison. @@ -938,7 +944,7 @@ class IterableList(list): can be left out.""" __slots__ = ('_id_attr', '_prefix') - def __new__(cls, id_attr: str, prefix: str = '') -> 'IterableList': + def __new__(cls, id_attr: str, prefix: str = '') -> 'IterableList[IterableObj]': return super(IterableList, cls).__new__(cls) def __init__(self, id_attr: str, prefix: str = '') -> None: @@ -1015,7 +1021,7 @@ class Iterable(object): _id_attribute_ = "attribute that most suitably identifies your instance" @classmethod - def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> 'IterableList': + def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList['IterableObj']: """ Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional @@ -1024,12 +1030,12 @@ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> 'IterableList': :note: Favor the iter_items method as it will :return:list(Item,...) list of item instances""" - out_list = IterableList(cls._id_attribute_) + out_list: IterableList = IterableList(cls._id_attribute_) out_list.extend(cls.iter_items(repo, *args, **kwargs)) return out_list @classmethod - def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator[TBD]: + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator: # return typed to be compatible with subtypes e.g. Remote """For more information about the arguments, see list_items :return: iterator yielding Items""" @@ -1038,6 +1044,10 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator[TBD]: #} END classes +class IterableObj(Iterable): + pass + + class NullHandler(logging.Handler): def emit(self, record: object) -> None: pass From 3cef949913659584dd980f3de363dd830392bb68 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 15:32:25 +0100 Subject: [PATCH 04/26] Rename Iterable due to typing.Iterable. Add deprecation warning --- git/util.py | 44 +++++++++++++++++++++++++++++++++++++++----- t.py | 19 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 t.py diff --git a/git/util.py b/git/util.py index 5f184b7a2..f72cd355a 100644 --- a/git/util.py +++ b/git/util.py @@ -18,6 +18,7 @@ import time from unittest import SkipTest from urllib.parse import urlsplit, urlunsplit +import warnings # typing --------------------------------------------------------- @@ -1013,15 +1014,52 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any: list.__delitem__(self, delindex) +class IterableClassWatcher(type): + def __init__(cls, name, bases, clsdict): + for base in bases: + if type(base) == cls: + warnings.warn("GitPython Iterable is deprecated due to naming clash. Use IterableObj instead", + DeprecationWarning) + super(IterableClassWatcher, cls).__init__(name, bases, clsdict) + + class Iterable(object): """Defines an interface for iterable items which is to assure a uniform way to retrieve and iterate items within the git repository""" __slots__ = () _id_attribute_ = "attribute that most suitably identifies your instance" + __metaclass__ = IterableClassWatcher @classmethod - def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList['IterableObj']: + def list_items(cls, repo, *args, **kwargs): + """ + Find all items of this type - subclasses can specify args and kwargs differently. + If no args are given, subclasses are obliged to return all items if no additional + arguments arg given. + + :note: Favor the iter_items method as it will + :return:list(Item,...) list of item instances""" + out_list = IterableList(cls._id_attribute_) + out_list.extend(cls.iter_items(repo, *args, **kwargs)) + return out_list + + @classmethod + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any): + # return typed to be compatible with subtypes e.g. Remote + """For more information about the arguments, see list_items + :return: iterator yielding Items""" + raise NotImplementedError("To be implemented by Subclass") + + +class IterableObj(): + """Defines an interface for iterable items which is to assure a uniform + way to retrieve and iterate items within the git repository""" + __slots__ = () + _id_attribute_ = "attribute that most suitably identifies your instance" + + @classmethod + def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T]: """ Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional @@ -1044,10 +1082,6 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator: #} END classes -class IterableObj(Iterable): - pass - - class NullHandler(logging.Handler): def emit(self, record: object) -> None: pass diff --git a/t.py b/t.py new file mode 100644 index 000000000..05d59c0cf --- /dev/null +++ b/t.py @@ -0,0 +1,19 @@ +class Watcher(type): + def __init__(cls, name, bases, clsdict): + [print("ooooo") for base in bases if issubclass(base, name)] + super(Watcher, cls).__init__(name, bases, clsdict) + + +class SuperClass(metaclass=Watcher): + pass + + +class SubClass0(SuperClass): + pass + + +class SubClass1(SuperClass): + print("test") + +class normo(): + print("wooo") From ae9d56e0fdd4df335a9def66aa2ac96459ed6e5c Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:05:03 +0100 Subject: [PATCH 05/26] Make Iterable deprecation warning on subclassing --- git/util.py | 10 ++++++---- t.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/git/util.py b/git/util.py index f72cd355a..78a60c9ae 100644 --- a/git/util.py +++ b/git/util.py @@ -1017,10 +1017,12 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any: class IterableClassWatcher(type): def __init__(cls, name, bases, clsdict): for base in bases: - if type(base) == cls: - warnings.warn("GitPython Iterable is deprecated due to naming clash. Use IterableObj instead", - DeprecationWarning) - super(IterableClassWatcher, cls).__init__(name, bases, clsdict) + if type(base) == IterableClassWatcher: + warnings.warn(f"GitPython Iterable subclassed by {name}. " + "Iterable is deprecated due to naming clash, " + "Use IterableObj instead \n", + DeprecationWarning, + stacklevel=2) class Iterable(object): diff --git a/t.py b/t.py index 05d59c0cf..215c26674 100644 --- a/t.py +++ b/t.py @@ -1,7 +1,15 @@ +import warnings + + class Watcher(type): def __init__(cls, name, bases, clsdict): - [print("ooooo") for base in bases if issubclass(base, name)] - super(Watcher, cls).__init__(name, bases, clsdict) + for base in bases: + if type(base) == Watcher: + warnings.warn(f"GitPython Iterable subclassed by {name}. " + "Iterable is deprecated due to naming clash, " + "Use IterableObj instead \n", + DeprecationWarning, + stacklevel=2) class SuperClass(metaclass=Watcher): @@ -15,5 +23,6 @@ class SubClass0(SuperClass): class SubClass1(SuperClass): print("test") + class normo(): print("wooo") From 8bf00a6719804c2fc5cca280e9dae6774acc1237 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:16:32 +0100 Subject: [PATCH 06/26] fix an import --- git/objects/submodule/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 57396a467..ce0f944e0 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -31,7 +31,8 @@ to_native_path_linux, RemoteProgress, rmtree, - unbare_repo + unbare_repo, + IterableList ) from git.util import HIDE_WINDOWS_KNOWN_ERRORS @@ -48,10 +49,6 @@ # typing ---------------------------------------------------------------------- -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from git.util import IterableList # ----------------------------------------------------------------------------- From 26dfeb66be61e9a2a9087bdecc98d255c0306079 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:23:15 +0100 Subject: [PATCH 07/26] fix indent --- git/objects/util.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index a565cf42f..71137264a 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -284,28 +284,6 @@ class Traversable(object): """ __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): """ From 4f5d2fd68e784c2b2fd914a196c66960c7f48b49 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:30:32 +0100 Subject: [PATCH 08/26] update docstring --- git/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git/util.py b/git/util.py index 78a60c9ae..79952be56 100644 --- a/git/util.py +++ b/git/util.py @@ -1036,11 +1036,13 @@ class Iterable(object): @classmethod def list_items(cls, repo, *args, **kwargs): """ + Deprecaated, use IterableObj instead. Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional arguments arg given. :note: Favor the iter_items method as it will + :return:list(Item,...) list of item instances""" out_list = IterableList(cls._id_attribute_) out_list.extend(cls.iter_items(repo, *args, **kwargs)) From d9f9027779931c3cdb04d570df5f01596539791b Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 17:09:51 +0100 Subject: [PATCH 09/26] update some TBDs to configparser --- git/objects/submodule/base.py | 2 +- git/util.py | 35 ++++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index ce0f944e0..cbf6cd0db 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1158,7 +1158,7 @@ def name(self): """ return self._name - def config_reader(self): + def config_reader(self) -> SectionConstraint: """ :return: ConfigReader instance which allows you to qurey the configuration values of this submodule, as provided by the .gitmodules file diff --git a/git/util.py b/git/util.py index 79952be56..245f45d1f 100644 --- a/git/util.py +++ b/git/util.py @@ -30,6 +30,8 @@ if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo + from git.config import GitConfigParser, SectionConstraint + from .types import PathLike, TBD, Literal, SupportsIndex # --------------------------------------------------------------------- @@ -82,7 +84,7 @@ def unbare_repo(func: Callable) -> Callable: encounter a bare repository""" @wraps(func) - def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> TBD: + def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> Callable: if self.repo.bare: raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__) # END bare method @@ -108,7 +110,7 @@ def rmtree(path: PathLike) -> None: :note: we use shutil rmtree but adjust its behaviour to see whether files that couldn't be deleted are read-only. Windows will not remove them in that case""" - def onerror(func: Callable, path: PathLike, exc_info: TBD) -> None: + def onerror(func: Callable, path: PathLike, exc_info: str) -> None: # Is the error an access error ? os.chmod(path, stat.S_IWUSR) @@ -448,7 +450,7 @@ class RemoteProgress(object): re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)") def __init__(self) -> None: - self._seen_ops = [] # type: List[TBD] + self._seen_ops = [] # type: List[int] self._cur_line = None # type: Optional[str] self.error_lines = [] # type: List[str] self.other_lines = [] # type: List[str] @@ -669,7 +671,8 @@ def _from_string(cls, string: str) -> 'Actor': # END handle name/email matching @classmethod - def _main_actor(cls, env_name: str, env_email: str, config_reader: Optional[TBD] = None) -> 'Actor': + def _main_actor(cls, env_name: str, env_email: str, + config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': actor = Actor('', '') user_id = None # We use this to avoid multiple calls to getpass.getuser() @@ -698,7 +701,7 @@ def default_name() -> str: return actor @classmethod - def committer(cls, config_reader: Optional[TBD] = None) -> 'Actor': + def committer(cls, config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': """ :return: Actor instance corresponding to the configured committer. It behaves similar to the git implementation, such that the environment will override @@ -709,7 +712,7 @@ def committer(cls, config_reader: Optional[TBD] = None) -> 'Actor': return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) @classmethod - def author(cls, config_reader: Optional[TBD] = None) -> 'Actor': + def author(cls, config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': """Same as committer(), but defines the main author. It may be specified in the environment, but defaults to the committer""" return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader) @@ -752,9 +755,14 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': """Create a Stat object from output retrieved by git-diff. :return: git.Stat""" - hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, - 'files': {} - } # type: Dict[str, Dict[str, TBD]] ## need typeddict or refactor for mypy + + # hsh: Dict[str, Dict[str, Union[int, Dict[str, int]]]] + hsh: Dict[str, Dict[str, TBD]] = {'total': {'insertions': 0, + 'deletions': 0, + 'lines': 0, + 'files': 0}, + 'files': {} + } # need typeddict? for line in text.splitlines(): (raw_insertions, raw_deletions, filename) = line.split("\t") insertions = raw_insertions != '-' and int(raw_insertions) or 0 @@ -763,9 +771,10 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': hsh['total']['deletions'] += deletions hsh['total']['lines'] += insertions + deletions hsh['total']['files'] += 1 - hsh['files'][filename.strip()] = {'insertions': insertions, - 'deletions': deletions, - 'lines': insertions + deletions} + files_dict = {'insertions': insertions, + 'deletions': deletions, + 'lines': insertions + deletions} + hsh['files'][filename.strip()] = files_dict return Stats(hsh['total'], hsh['files']) @@ -1077,7 +1086,7 @@ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T]: return out_list @classmethod - def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator: + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator[T]: # return typed to be compatible with subtypes e.g. Remote """For more information about the arguments, see list_items :return: iterator yielding Items""" From affee359af09cf7971676263f59118de82e7e059 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 17:35:22 +0100 Subject: [PATCH 10/26] Add typedDict --- git/types.py | 24 +++++++++++++++++++++--- git/util.py | 25 +++++++++++++------------ t.py | 28 ---------------------------- 3 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 t.py diff --git a/git/types.py b/git/types.py index a410cb366..8c431e53e 100644 --- a/git/types.py +++ b/git/types.py @@ -4,12 +4,12 @@ import os import sys -from typing import Union, Any +from typing import Dict, Union, Any if sys.version_info[:2] >= (3, 8): - from typing import Final, Literal, SupportsIndex # noqa: F401 + from typing import Final, Literal, SupportsIndex, TypedDict # noqa: F401 else: - from typing_extensions import Final, Literal, SupportsIndex # noqa: F401 + from typing_extensions import Final, Literal, SupportsIndex, TypedDict # noqa: F401 if sys.version_info[:2] < (3, 9): @@ -22,3 +22,21 @@ TBD = Any Lit_config_levels = Literal['system', 'global', 'user', 'repository'] + + +class Files_TD(TypedDict): + insertions: int + deletions: int + lines: int + + +class Total_TD(TypedDict): + insertions: int + deletions: int + lines: int + files: int + + +class HSH_TD(TypedDict): + total: Total_TD + files: Dict[str, Files_TD] diff --git a/git/util.py b/git/util.py index 245f45d1f..0783918d1 100644 --- a/git/util.py +++ b/git/util.py @@ -32,7 +32,7 @@ from git.repo.base import Repo from git.config import GitConfigParser, SectionConstraint -from .types import PathLike, TBD, Literal, SupportsIndex +from .types import PathLike, Literal, SupportsIndex, HSH_TD, Files_TD # --------------------------------------------------------------------- @@ -746,7 +746,9 @@ class Stats(object): files = number of changed files as int""" __slots__ = ("total", "files") - def __init__(self, total: Dict[str, Dict[str, int]], files: Dict[str, Dict[str, int]]): + from git.types import Total_TD, Files_TD + + def __init__(self, total: Total_TD, files: Dict[str, Files_TD]): self.total = total self.files = files @@ -756,13 +758,12 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': :return: git.Stat""" - # hsh: Dict[str, Dict[str, Union[int, Dict[str, int]]]] - hsh: Dict[str, Dict[str, TBD]] = {'total': {'insertions': 0, - 'deletions': 0, - 'lines': 0, - 'files': 0}, - 'files': {} - } # need typeddict? + hsh: HSH_TD = {'total': {'insertions': 0, + 'deletions': 0, + 'lines': 0, + 'files': 0}, + 'files': {} + } for line in text.splitlines(): (raw_insertions, raw_deletions, filename) = line.split("\t") insertions = raw_insertions != '-' and int(raw_insertions) or 0 @@ -771,9 +772,9 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': hsh['total']['deletions'] += deletions hsh['total']['lines'] += insertions + deletions hsh['total']['files'] += 1 - files_dict = {'insertions': insertions, - 'deletions': deletions, - 'lines': insertions + deletions} + files_dict: Files_TD = {'insertions': insertions, + 'deletions': deletions, + 'lines': insertions + deletions} hsh['files'][filename.strip()] = files_dict return Stats(hsh['total'], hsh['files']) diff --git a/t.py b/t.py deleted file mode 100644 index 215c26674..000000000 --- a/t.py +++ /dev/null @@ -1,28 +0,0 @@ -import warnings - - -class Watcher(type): - def __init__(cls, name, bases, clsdict): - for base in bases: - if type(base) == Watcher: - warnings.warn(f"GitPython Iterable subclassed by {name}. " - "Iterable is deprecated due to naming clash, " - "Use IterableObj instead \n", - DeprecationWarning, - stacklevel=2) - - -class SuperClass(metaclass=Watcher): - pass - - -class SubClass0(SuperClass): - pass - - -class SubClass1(SuperClass): - print("test") - - -class normo(): - print("wooo") From fe594eb345fbefaee3b82436183d6560991724cc Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:14:13 +0100 Subject: [PATCH 11/26] Add T_Tre_cache TypeVar --- git/objects/tree.py | 32 ++++++++++++++++++-------------- git/types.py | 2 +- git/util.py | 6 +++--- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/git/objects/tree.py b/git/objects/tree.py index ec7d8e885..97a4b7485 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -20,7 +20,7 @@ # typing ------------------------------------------------- -from typing import Callable, Dict, Iterable, Iterator, List, Tuple, Type, Union, cast, TYPE_CHECKING +from typing import Callable, Dict, Generic, Iterable, Iterator, List, Tuple, Type, TypeVar, Union, cast, TYPE_CHECKING from git.types import PathLike @@ -31,13 +31,16 @@ #-------------------------------------------------------- -cmp: Callable[[int, int], int] = lambda a, b: (a > b) - (a < b) +cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b) __all__ = ("TreeModifier", "Tree") +T_Tree_cache = TypeVar('T_Tree_cache', bound=Union[Tuple[bytes, int, str]]) -def git_cmp(t1: 'Tree', t2: 'Tree') -> int: + +def git_cmp(t1: T_Tree_cache, t2: T_Tree_cache) -> int: a, b = t1[2], t2[2] + assert isinstance(a, str) and isinstance(b, str) # Need as mypy 9.0 cannot unpack TypeVar properly len_a, len_b = len(a), len(b) min_len = min(len_a, len_b) min_cmp = cmp(a[:min_len], b[:min_len]) @@ -48,7 +51,8 @@ def git_cmp(t1: 'Tree', t2: 'Tree') -> int: return len_a - len_b -def merge_sort(a: List[int], cmp: Callable[[int, int], int]) -> None: +def merge_sort(a: List[T_Tree_cache], + cmp: Callable[[T_Tree_cache, T_Tree_cache], int]) -> None: if len(a) < 2: return None @@ -83,7 +87,7 @@ def merge_sort(a: List[int], cmp: Callable[[int, int], int]) -> None: k = k + 1 -class TreeModifier(object): +class TreeModifier(Generic[T_Tree_cache], object): """A utility class providing methods to alter the underlying cache in a list-like fashion. @@ -91,10 +95,10 @@ class TreeModifier(object): the cache of a tree, will be sorted. Assuring it will be in a serializable state""" __slots__ = '_cache' - def __init__(self, cache): + def __init__(self, cache: List[T_Tree_cache]) -> None: self._cache = cache - def _index_by_name(self, name): + def _index_by_name(self, name: str) -> int: """:return: index of an item with name, or -1 if not found""" for i, t in enumerate(self._cache): if t[2] == name: @@ -104,7 +108,7 @@ def _index_by_name(self, name): return -1 #{ Interface - def set_done(self): + def set_done(self) -> 'TreeModifier': """Call this method once you are done modifying the tree information. It may be called several times, but be aware that each call will cause a sort operation @@ -114,7 +118,7 @@ def set_done(self): #} END interface #{ Mutators - def add(self, sha, mode, name, force=False): + def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeModifier': """Add the given item to the tree. If an item with the given name already exists, nothing will be done, but a ValueError will be raised if the sha and mode of the existing item do not match the one you add, unless @@ -132,7 +136,7 @@ def add(self, sha, mode, name, force=False): sha = to_bin_sha(sha) index = self._index_by_name(name) - item = (sha, mode, name) + item: T_Tree_cache = (sha, mode, name) # type: ignore ## use Typeguard from typing-extensions 3.10.0 if index == -1: self._cache.append(item) else: @@ -195,7 +199,7 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable): def __init__(self, repo: 'Repo', binsha: bytes, mode: int = tree_id << 12, path: Union[PathLike, None] = None): super(Tree, self).__init__(repo, binsha, mode, path) - @classmethod + @ classmethod def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore ) -> Union[Tuple['Tree', ...], Tuple[()]]: if index_object.type == "tree": @@ -261,17 +265,17 @@ def __truediv__(self, file: str) -> Union['Tree', Blob, Submodule]: """For PY3 only""" return self.join(file) - @property + @ property def trees(self) -> List['Tree']: """:return: list(Tree, ...) list of trees directly below this tree""" return [i for i in self if i.type == "tree"] - @property + @ property def blobs(self) -> List['Blob']: """:return: list(Blob, ...) list of blobs directly below this tree""" return [i for i in self if i.type == "blob"] - @property + @ property def cache(self) -> TreeModifier: """ :return: An object allowing to modify the internal cache. This can be used diff --git a/git/types.py b/git/types.py index 8c431e53e..c01ea27e1 100644 --- a/git/types.py +++ b/git/types.py @@ -39,4 +39,4 @@ class Total_TD(TypedDict): class HSH_TD(TypedDict): total: Total_TD - files: Dict[str, Files_TD] + files: Dict[PathLike, Files_TD] diff --git a/git/util.py b/git/util.py index 0783918d1..bcc634ec1 100644 --- a/git/util.py +++ b/git/util.py @@ -672,7 +672,7 @@ def _from_string(cls, string: str) -> 'Actor': @classmethod def _main_actor(cls, env_name: str, env_email: str, - config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': + config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor': actor = Actor('', '') user_id = None # We use this to avoid multiple calls to getpass.getuser() @@ -701,7 +701,7 @@ def default_name() -> str: return actor @classmethod - def committer(cls, config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': + def committer(cls, config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor': """ :return: Actor instance corresponding to the configured committer. It behaves similar to the git implementation, such that the environment will override @@ -748,7 +748,7 @@ class Stats(object): from git.types import Total_TD, Files_TD - def __init__(self, total: Total_TD, files: Dict[str, Files_TD]): + def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]): self.total = total self.files = files From 59c89441fb81b0f4549e4bf7ab01f4c27da54aad Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:37:41 +0100 Subject: [PATCH 12/26] forward ref Gitconfigparser --- git/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index bcc634ec1..eccaa74ed 100644 --- a/git/util.py +++ b/git/util.py @@ -712,7 +712,7 @@ def committer(cls, config_reader: Union[None, 'GitConfigParser', 'SectionConstra return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) @classmethod - def author(cls, config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': + def author(cls, config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor': """Same as committer(), but defines the main author. It may be specified in the environment, but defaults to the committer""" return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader) From b72118e231c7bc42f457e2b02e0f90e8f87a5794 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:45:14 +0100 Subject: [PATCH 13/26] Import TypeGuard to replace casts --- git/types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/git/types.py b/git/types.py index c01ea27e1..e3b49170d 100644 --- a/git/types.py +++ b/git/types.py @@ -11,6 +11,11 @@ else: from typing_extensions import Final, Literal, SupportsIndex, TypedDict # noqa: F401 +if sys.version_info[:2] >= (3, 10): + from typing import TypeGuard # noqa: F401 +else: + from typing_extensions import TypeGuard # noqa: F401 + if sys.version_info[:2] < (3, 9): # Python >= 3.6, < 3.9 From fb3fec340f89955a4b0adfd64636d26300d22af9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:56:29 +0100 Subject: [PATCH 14/26] Update typing-extensions dependancy to =4.0.1,<5 -typing-extensions>=3.7.4.3;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.10" diff --git a/test-requirements.txt b/test-requirements.txt index 16dc0d2c1..ab3f86109 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.3;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.10" From a2d9011c05b0e27f1324f393e65954542544250d Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 00:06:15 +0100 Subject: [PATCH 15/26] Add asserts and casts for T_Tree_cache --- git/objects/tree.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/git/objects/tree.py b/git/objects/tree.py index 97a4b7485..191fe27c3 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -136,7 +136,9 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeMod sha = to_bin_sha(sha) index = self._index_by_name(name) - item: T_Tree_cache = (sha, mode, name) # type: ignore ## use Typeguard from typing-extensions 3.10.0 + + assert isinstance(sha, bytes) and isinstance(mode, int) and isinstance(name, str) + item = cast(T_Tree_cache, (sha, mode, name)) # use Typeguard from typing-extensions 3.10.0 if index == -1: self._cache.append(item) else: @@ -151,14 +153,17 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeMod # END handle name exists return self - def add_unchecked(self, binsha, mode, name): + def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None: """Add the given item to the tree, its correctness is assumed, which puts the caller into responsibility to assure the input is correct. For more information on the parameters, see ``add`` :param binsha: 20 byte binary sha""" - self._cache.append((binsha, mode, name)) + assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str) + tree_cache = cast(T_Tree_cache, (binsha, mode, name)) + + self._cache.append(tree_cache) - def __delitem__(self, name): + def __delitem__(self, name: str) -> None: """Deletes an item with the given name if it exists""" index = self._index_by_name(name) if index > -1: From 0eae33d324376a0a1800e51bddf7f23a343f45a1 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:21:59 +0100 Subject: [PATCH 16/26] Add is_flatLiteral() Typeguard[] to remote.py --- git/remote.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/git/remote.py b/git/remote.py index a85297c17..2bf64150f 100644 --- a/git/remote.py +++ b/git/remote.py @@ -38,7 +38,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, cast, overload -from git.types import PathLike, Literal, TBD +from git.types import PathLike, Literal, TBD, TypeGuard if TYPE_CHECKING: from git.repo.base import Repo @@ -48,8 +48,15 @@ from git.objects.tag import TagObject flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't'] + + +def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: + return inp in [' ', '!', '+', '-', '=', '*', 't'] + + # ------------------------------------------------------------- + log = logging.getLogger('git.remote') log.addHandler(logging.NullHandler()) @@ -325,7 +332,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': # parse lines control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() - control_character = cast(flagKeyLiteral, control_character) # can do this neater once 3.5 dropped + assert is_flagKeyLiteral(control_character) try: _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") From 5b0465c9bcca64c3a863a95735cc5e602946facb Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:27:22 +0100 Subject: [PATCH 17/26] fix assert --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index 2bf64150f..748dcbbd3 100644 --- a/git/remote.py +++ b/git/remote.py @@ -332,7 +332,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': # parse lines control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() - assert is_flagKeyLiteral(control_character) + assert is_flagKeyLiteral(control_character), f"{control_character}" try: _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") From dc8d23d3d6e735d70fd0a60641c58f6e44e17029 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:30:44 +0100 Subject: [PATCH 18/26] Add '?' to controlcharacter literal --- git/remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/remote.py b/git/remote.py index 748dcbbd3..e6daffe0c 100644 --- a/git/remote.py +++ b/git/remote.py @@ -47,11 +47,11 @@ from git.objects.tree import Tree from git.objects.tag import TagObject -flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't'] +flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't', '?'] def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: - return inp in [' ', '!', '+', '-', '=', '*', 't'] + return inp in [' ', '!', '+', '-', '=', '*', 't', '?'] # ------------------------------------------------------------- From 7b09003fffa8196277bcfaa9984a3e6833805a6d Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:52:29 +0100 Subject: [PATCH 19/26] replace cast()s with asserts in remote.py --- git/refs/log.py | 12 ++++++------ git/remote.py | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/git/refs/log.py b/git/refs/log.py index 363c3c5d5..f850ba24c 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -82,23 +82,23 @@ def new(cls, oldhexsha, newhexsha, actor, time, tz_offset, message): # skipcq: return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message)) @classmethod - def from_line(cls, line): + def from_line(cls, line: bytes) -> 'RefLogEntry': """:return: New RefLogEntry instance from the given revlog line. :param line: line bytes without trailing newline :raise ValueError: If line could not be parsed""" - line = line.decode(defenc) - fields = line.split('\t', 1) + line_str = line.decode(defenc) + fields = line_str.split('\t', 1) if len(fields) == 1: info, msg = fields[0], None elif len(fields) == 2: info, msg = fields else: raise ValueError("Line must have up to two TAB-separated fields." - " Got %s" % repr(line)) + " Got %s" % repr(line_str)) # END handle first split - oldhexsha = info[:40] # type: str - newhexsha = info[41:81] # type: str + oldhexsha = info[:40] + newhexsha = info[41:81] for hexsha in (oldhexsha, newhexsha): if not cls._re_hexsha_only.match(hexsha): raise ValueError("Invalid hexsha: %r" % (hexsha,)) diff --git a/git/remote.py b/git/remote.py index e6daffe0c..a6232db32 100644 --- a/git/remote.py +++ b/git/remote.py @@ -36,7 +36,7 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, cast, overload +from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, overload from git.types import PathLike, Literal, TBD, TypeGuard @@ -559,8 +559,8 @@ def delete_url(self, url: str, **kwargs: Any) -> 'Remote': def urls(self) -> Iterator[str]: """:return: Iterator yielding all configured URL targets on a remote as strings""" try: - # can replace cast with type assert? - remote_details = cast(str, self.repo.git.remote("get-url", "--all", self.name)) + remote_details = self.repo.git.remote("get-url", "--all", self.name) + assert isinstance(remote_details, str) for line in remote_details.split('\n'): yield line except GitCommandError as ex: @@ -571,14 +571,16 @@ def urls(self) -> Iterator[str]: # if 'Unknown subcommand: get-url' in str(ex): try: - remote_details = cast(str, self.repo.git.remote("show", self.name)) + remote_details = self.repo.git.remote("show", self.name) + assert isinstance(remote_details, str) for line in remote_details.split('\n'): if ' Push URL:' in line: yield line.split(': ')[-1] except GitCommandError as _ex: if any(msg in str(_ex) for msg in ['correct access rights', 'cannot run ssh']): # If ssh is not setup to access this repository, see issue 694 - remote_details = cast(str, self.repo.git.config('--get-all', 'remote.%s.url' % self.name)) + remote_details = self.repo.git.config('--get-all', 'remote.%s.url' % self.name) + assert isinstance(remote_details, str) for line in remote_details.split('\n'): yield line else: From aba4d9b4029373d2bccc961a23134454072936ce Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 21:03:17 +0100 Subject: [PATCH 20/26] replace cast()s with asserts in fun.py --- git/index/fun.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index 10a440501..ffd109b1f 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -53,7 +53,7 @@ from typing import (Dict, IO, List, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast) -from git.types import PathLike +from git.types import PathLike, TypeGuard if TYPE_CHECKING: from .base import IndexFile @@ -185,11 +185,17 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]: def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]: """:return: Key suitable to be used for the index.entries dictionary :param entry: One instance of type BaseIndexEntry or the path and the stage""" + + def is_entry_tuple(entry: Tuple) -> TypeGuard[Tuple[PathLike, int]]: + return isinstance(entry, tuple) and len(entry) == 2 + if len(entry) == 1: - entry_first = cast(BaseIndexEntry, entry[0]) # type: BaseIndexEntry + entry_first = entry[0] + assert isinstance(entry_first, BaseIndexEntry) return (entry_first.path, entry_first.stage) else: - entry = cast(Tuple[PathLike, int], tuple(entry)) + # entry = tuple(entry) + assert is_entry_tuple(entry) return entry # END handle entry From 07bfe1a60ae93d8b40c9aa01a3775f334d680daa Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 21:23:03 +0100 Subject: [PATCH 21/26] trigger checks to rurun --- git/objects/submodule/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py index 0b4ce3c53..045fb47d6 100644 --- a/git/objects/submodule/util.py +++ b/git/objects/submodule/util.py @@ -65,7 +65,7 @@ def set_submodule(self, submodule): the first write operation begins""" self._smref = weakref.ref(submodule) - def flush_to_index(self): + def flush_to_index(self) -> None: """Flush changes in our configuration file to the index""" assert self._smref is not None # should always have a file here From 09fb2274db09e44bf3bc14da482ffa9a98659c54 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 21:29:55 +0100 Subject: [PATCH 22/26] Add type to submodule to trigger checks to rurun --- git/objects/submodule/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py index 045fb47d6..b4796b300 100644 --- a/git/objects/submodule/util.py +++ b/git/objects/submodule/util.py @@ -4,6 +4,11 @@ from io import BytesIO import weakref +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .base import Submodule + __all__ = ('sm_section', 'sm_name', 'mkhead', 'find_first_remote_branch', 'SubmoduleConfigParser') @@ -60,7 +65,7 @@ def __init__(self, *args, **kwargs): super(SubmoduleConfigParser, self).__init__(*args, **kwargs) #{ Interface - def set_submodule(self, submodule): + def set_submodule(self, submodule: 'Submodule') -> None: """Set this instance's submodule. It must be called before the first write operation begins""" self._smref = weakref.ref(submodule) From eff48b8ba25a0ea36a7286aa16d8888315eb1205 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:38:42 +0100 Subject: [PATCH 23/26] Import typevar in util.py --- git/objects/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 71137264a..7736a0b23 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,7 +19,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, TYPE_CHECKING, Tuple, Type, Union, cast) +from typing import (Any, Callable, Deque, Iterator, typevar, TYPE_CHECKING, Tuple, Type, Union, cast) if TYPE_CHECKING: from io import BytesIO, StringIO @@ -29,6 +29,8 @@ from .tag import TagObject from .tree import Tree from subprocess import Popen + +T_Iterableobj = typevar('T_Iterableobj', bound=T_Iterableobj) # -------------------------------------------------------------------- From 17c750a0803ae222f1cdaf3d6282a7e1b2046adb Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:40:41 +0100 Subject: [PATCH 24/26] flake8 fix --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 7736a0b23..79bf73aea 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -30,7 +30,7 @@ from .tree import Tree from subprocess import Popen -T_Iterableobj = typevar('T_Iterableobj', bound=T_Iterableobj) +T_Iterableobj = typevar('T_Iterableobj') # -------------------------------------------------------------------- From ff56dbbfceef2211087aed2619b7da2e42f235e4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:43:10 +0100 Subject: [PATCH 25/26] fix typo --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 79bf73aea..4609a80b1 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,7 +19,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, typevar, TYPE_CHECKING, Tuple, Type, Union, cast) +from typing import (Any, Callable, Deque, Iterator, TypeVar, TYPE_CHECKING, Tuple, Type, Union, cast) if TYPE_CHECKING: from io import BytesIO, StringIO From 5d7b8ba9f2e9298496232e4ae66bd904a1d71001 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:44:54 +0100 Subject: [PATCH 26/26] another typo --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 4609a80b1..8b8148a9f 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -30,7 +30,7 @@ from .tree import Tree from subprocess import Popen -T_Iterableobj = typevar('T_Iterableobj') +T_Iterableobj = TypeVar('T_Iterableobj') # --------------------------------------------------------------------